@ateam-ai-mobile/ui 0.1.8 → 0.1.10

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
@@ -1,5 +1,5 @@
1
1
  // src/AdasChatScreen.tsx
2
- import { useCallback as useCallback5, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef4, useState as useState6 } from "react";
2
+ import { useCallback as useCallback5, useEffect as useEffect7, useMemo as useMemo3, useRef as useRef4, useState as useState6 } from "react";
3
3
  import {
4
4
  FlatList,
5
5
  KeyboardAvoidingView,
@@ -238,13 +238,13 @@ function getPluginWebViewExtraHosts() {
238
238
  }
239
239
 
240
240
  // src/plugin/WidgetHost.tsx
241
- import { useEffect as useEffect4 } from "react";
241
+ import { useEffect as useEffect5 } from "react";
242
242
  import { create } from "zustand";
243
243
  import { pluginCommandEmitter as pluginCommandEmitter2 } from "@ateam-ai-mobile/core";
244
244
  import { useSurfaceStore } from "@ateam-ai-mobile/plugin-runtime";
245
245
 
246
246
  // src/plugin/PluginSurface.tsx
247
- import { useEffect as useEffect3, useState as useState3 } from "react";
247
+ import { useEffect as useEffect4, useState as useState3 } from "react";
248
248
  import { View as View4, ActivityIndicator as ActivityIndicator3 } from "react-native";
249
249
  import { ada as ada3 } from "@ateam-ai-mobile/core";
250
250
  import {
@@ -265,7 +265,7 @@ import {
265
265
  } from "@ateam-ai-mobile/plugin-runtime";
266
266
 
267
267
  // src/plugin/EmbeddedPluginWebView.tsx
268
- import { useState as useState2, useMemo, useCallback as useCallback2 } from "react";
268
+ import { useState as useState2, useMemo, useCallback as useCallback2, useEffect as useEffect3 } from "react";
269
269
  import { View as View2, Text as Text2, ActivityIndicator, StyleSheet as StyleSheet2, Platform } from "react-native";
270
270
  import { WebView } from "react-native-webview";
271
271
  import { getConfig, getCoreApiUrl, getVoiceApiUrl, getTenant as getTenant2, getToken, ada as ada2 } from "@ateam-ai-mobile/core";
@@ -596,6 +596,9 @@ function EmbeddedPluginWebView({ pluginId, manifest, commandBufferRef }) {
596
596
  const tenant = getTenant2();
597
597
  const { webViewRef, sendInit, handleMessage } = usePluginBridge(pluginId, manifest._connectorId, commandBufferRef);
598
598
  const url = resolvePluginUrl(manifest, tenant || void 0);
599
+ useEffect3(() => {
600
+ setError(null);
601
+ }, [url]);
599
602
  const bridgeJS = buildPluginBridgeJS(token || "", tenant || "");
600
603
  const computeAllowedHosts = useCallback2(() => {
601
604
  const hosts = /* @__PURE__ */ new Set();
@@ -833,7 +836,7 @@ import { jsx as jsx4 } from "react/jsx-runtime";
833
836
  function PluginSurface({ pluginId, surface, dismiss, props }) {
834
837
  const [manifest, setManifest] = useState3(null);
835
838
  const [error, setError] = useState3(null);
836
- useEffect3(() => {
839
+ useEffect4(() => {
837
840
  let cancelled = false;
838
841
  getPlugin(pluginId).then((m) => {
839
842
  if (!cancelled) m ? setManifest(m) : setError("Plugin not found");
@@ -892,7 +895,7 @@ function WidgetHost() {
892
895
  const pluginId = useWidgetHostStore((s) => s.pluginId);
893
896
  const props = useWidgetHostStore((s) => s.props);
894
897
  const close = useWidgetHostStore((s) => s.close);
895
- useEffect4(() => {
898
+ useEffect5(() => {
896
899
  return pluginCommandEmitter2.on("focus-plugin", (data) => {
897
900
  if (data?.pluginId) openWidget(String(data.pluginId), data.props);
898
901
  });
@@ -1600,7 +1603,7 @@ var styles8 = StyleSheet8.create({
1600
1603
  });
1601
1604
 
1602
1605
  // src/chat/ChatInput.tsx
1603
- import { useState as useState5, useRef as useRef3, useCallback as useCallback4, useEffect as useEffect5 } from "react";
1606
+ import { useState as useState5, useRef as useRef3, useCallback as useCallback4, useEffect as useEffect6 } from "react";
1604
1607
  import {
1605
1608
  View as View10,
1606
1609
  TextInput,
@@ -1616,7 +1619,7 @@ function ChatInput({ onSend, onCallStart, loading, prefill, placeholder }) {
1616
1619
  const [text, setText] = useState5("");
1617
1620
  const [height, setHeight] = useState5(40);
1618
1621
  const inputRef = useRef3(null);
1619
- useEffect5(() => {
1622
+ useEffect6(() => {
1620
1623
  if (prefill) {
1621
1624
  setText(prefill);
1622
1625
  inputRef.current?.focus();
@@ -1777,10 +1780,10 @@ function AdasChatScreen({
1777
1780
  const listRef = useRef4(null);
1778
1781
  const [refreshing, setRefreshing] = useState6(false);
1779
1782
  const [prefill, setPrefill] = useState6("");
1780
- useEffect6(() => {
1783
+ useEffect7(() => {
1781
1784
  if (!conversationLoaded) loadConversation();
1782
1785
  }, [conversationLoaded, loadConversation]);
1783
- useEffect6(() => {
1786
+ useEffect7(() => {
1784
1787
  const unsub = pluginCommandEmitter3.on("prepare-chat-message", ({ text }) => {
1785
1788
  if (text) setPrefill(text);
1786
1789
  });
@@ -2004,7 +2007,7 @@ var styles12 = StyleSheet12.create({
2004
2007
  });
2005
2008
 
2006
2009
  // src/TenantPicker.tsx
2007
- import { useEffect as useEffect7, useState as useState8 } from "react";
2010
+ import { useEffect as useEffect8, useState as useState8 } from "react";
2008
2011
  import { View as View14, Text as Text10, Pressable as Pressable8, ActivityIndicator as ActivityIndicator5, StyleSheet as StyleSheet13, Platform as Platform9 } from "react-native";
2009
2012
  import { ada as ada10, fontSize as fontSize6, borderRadius as borderRadius4, listMyTenants, switchTenant, getTenant as getTenant4 } from "@ateam-ai-mobile/core";
2010
2013
  import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
@@ -2014,7 +2017,7 @@ function TenantPicker({ onSwitched, onSelect, currentTenantId }) {
2014
2017
  const [error, setError] = useState8(null);
2015
2018
  const [switching, setSwitching] = useState8(null);
2016
2019
  const active = currentTenantId ?? getTenant4();
2017
- useEffect7(() => {
2020
+ useEffect8(() => {
2018
2021
  let cancelled = false;
2019
2022
  listMyTenants().then((list) => {
2020
2023
  if (!cancelled) setTenants(list);
@@ -2185,7 +2188,7 @@ var styles14 = StyleSheet14.create({
2185
2188
  });
2186
2189
 
2187
2190
  // src/auth/HandoffScreen.tsx
2188
- import { useEffect as useEffect8, useRef as useRef5, useState as useState9 } from "react";
2191
+ import { useEffect as useEffect9, useRef as useRef5, useState as useState9 } from "react";
2189
2192
  import { View as View16, Text as Text12, Pressable as Pressable9, StyleSheet as StyleSheet15, Platform as Platform10, ActivityIndicator as ActivityIndicator6 } from "react-native";
2190
2193
  import { exchangeHandoff, ada as ada12 } from "@ateam-ai-mobile/core";
2191
2194
  import { AdaOrb as AdaOrb3 } from "@ateam-ai-mobile/plugin-runtime";
@@ -2193,7 +2196,6 @@ import { AdaOrb as AdaOrb3 } from "@ateam-ai-mobile/plugin-runtime";
2193
2196
  // src/auth/handoffExchange.ts
2194
2197
  async function runHandoffExchange(code, deps) {
2195
2198
  const res = await deps.exchange(code);
2196
- if (res.tenant) await deps.onAdoptTenant(res.tenant);
2197
2199
  await deps.onSignedIn(res.jwt);
2198
2200
  deps.onDone();
2199
2201
  }
@@ -2208,14 +2210,14 @@ function handoffErrorMessage(msg) {
2208
2210
  // src/auth/HandoffScreen.tsx
2209
2211
  import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
2210
2212
  var fontFam7 = Platform10.OS === "ios" ? "SF Pro Rounded" : void 0;
2211
- function HandoffScreen({ code, onAdoptTenant, onSignedIn, onDone, onCancel }) {
2213
+ function HandoffScreen({ code, onSignedIn, onDone, onCancel }) {
2212
2214
  const [err, setErr] = useState9(null);
2213
2215
  const startedForRef = useRef5(null);
2214
- useEffect8(() => {
2216
+ useEffect9(() => {
2215
2217
  if (!code || startedForRef.current === code) return;
2216
2218
  startedForRef.current = code;
2217
- runHandoffExchange(code, { exchange: exchangeHandoff, onAdoptTenant, onSignedIn, onDone }).catch((e) => setErr(handoffErrorMessage(e?.message)));
2218
- }, [code, onAdoptTenant, onSignedIn, onDone]);
2219
+ runHandoffExchange(code, { exchange: exchangeHandoff, onSignedIn, onDone }).catch((e) => setErr(handoffErrorMessage(e?.message)));
2220
+ }, [code, onSignedIn, onDone]);
2219
2221
  return /* @__PURE__ */ jsx17(View16, { style: styles15.container, children: /* @__PURE__ */ jsxs13(View16, { style: styles15.center, children: [
2220
2222
  /* @__PURE__ */ jsx17(AdaOrb3, { size: 72, state: err ? "idle" : "thinking" }),
2221
2223
  !err ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
@@ -2238,7 +2240,7 @@ var styles15 = StyleSheet15.create({
2238
2240
  });
2239
2241
 
2240
2242
  // src/auth/OpenJoinScreen.tsx
2241
- import { useEffect as useEffect9, useState as useState10 } from "react";
2243
+ import { useEffect as useEffect10, useState as useState10 } from "react";
2242
2244
  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
2245
  import { openJoinWithGoogle, openJoinWithApple, ada as ada13, spacing as spacing3 } from "@ateam-ai-mobile/core";
2244
2246
  import { AdaOrb as AdaOrb4 } from "@ateam-ai-mobile/plugin-runtime";
@@ -2247,7 +2249,6 @@ import { AdaOrb as AdaOrb4 } from "@ateam-ai-mobile/plugin-runtime";
2247
2249
  async function completeOpenJoin(slug, idToken, deps) {
2248
2250
  if (!idToken) return false;
2249
2251
  const consumer = await deps.join(slug, idToken);
2250
- await deps.onAdoptTenant(slug);
2251
2252
  await deps.onSignedIn(consumer.jwt);
2252
2253
  return true;
2253
2254
  }
@@ -2265,12 +2266,12 @@ function openJoinErrorMessage(msg) {
2265
2266
  // src/auth/OpenJoinScreen.tsx
2266
2267
  import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2267
2268
  var fontFam8 = Platform11.OS === "ios" ? "SF Pro Rounded" : void 0;
2268
- function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }) {
2269
+ function OpenJoinScreen({ onSignedIn, onBack }) {
2269
2270
  const [team, setTeam] = useState10("");
2270
2271
  const [busy, setBusy] = useState10(false);
2271
2272
  const [err, setErr] = useState10(null);
2272
2273
  const [appleAvailable, setAppleAvailable] = useState10(false);
2273
- useEffect9(() => {
2274
+ useEffect10(() => {
2274
2275
  getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2275
2276
  }, []);
2276
2277
  const slug = team.trim().toLowerCase();
@@ -2286,7 +2287,7 @@ function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }) {
2286
2287
  setBusy(true);
2287
2288
  try {
2288
2289
  const idToken = await acquire(oauth);
2289
- await completeOpenJoin(slug, idToken, { join, onAdoptTenant, onSignedIn });
2290
+ await completeOpenJoin(slug, idToken, { join, onSignedIn });
2290
2291
  } catch (e) {
2291
2292
  setErr(openJoinErrorMessage(e?.message));
2292
2293
  } finally {
@@ -2369,7 +2370,7 @@ var styles16 = StyleSheet16.create({
2369
2370
  });
2370
2371
 
2371
2372
  // src/auth/JoinScreen.tsx
2372
- import { useEffect as useEffect10, useMemo as useMemo5, useState as useState11 } from "react";
2373
+ import { useEffect as useEffect11, useMemo as useMemo5, useState as useState11 } from "react";
2373
2374
  import { View as View18, Text as Text14, Pressable as Pressable11, StyleSheet as StyleSheet17, ActivityIndicator as ActivityIndicator8, Platform as Platform12 } from "react-native";
2374
2375
  import {
2375
2376
  previewInvite,
@@ -2402,13 +2403,13 @@ function JoinScreen({ code, onSignedIn, onDone, onCancel }) {
2402
2403
  const [busy, setBusy] = useState11(false);
2403
2404
  const [err, setErr] = useState11(null);
2404
2405
  const [appleAvailable, setAppleAvailable] = useState11(false);
2405
- useEffect10(() => {
2406
+ useEffect11(() => {
2406
2407
  if (!code) return;
2407
2408
  setPreview(null);
2408
2409
  setLoadErr(null);
2409
2410
  previewInvite(code).then(setPreview).catch((e) => setLoadErr(e?.message || "Could not load invite"));
2410
2411
  }, [code]);
2411
- useEffect10(() => {
2412
+ useEffect11(() => {
2412
2413
  getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2413
2414
  }, []);
2414
2415
  const orgLine = useMemo5(() => inviteHeadline(preview), [preview]);
@@ -2487,8 +2488,8 @@ var styles17 = StyleSheet17.create({
2487
2488
  });
2488
2489
 
2489
2490
  // 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";
2491
+ import { useEffect as useEffect12, useMemo as useMemo6, useState as useState12 } from "react";
2492
+ import { View as View19, Text as Text15, Pressable as Pressable12, StyleSheet as StyleSheet18, Platform as Platform13, ActivityIndicator as ActivityIndicator9, TextInput as TextInput3 } from "react-native";
2492
2493
  import { fetchConsumerConfig, signInWithGoogle, signInWithApple, ada as ada15 } from "@ateam-ai-mobile/core";
2493
2494
  import { AdaOrb as AdaOrb6 } from "@ateam-ai-mobile/plugin-runtime";
2494
2495
 
@@ -2500,8 +2501,8 @@ function googleClientIds(config) {
2500
2501
  iosClientId: g.ios_client_id || ""
2501
2502
  };
2502
2503
  }
2503
- function shouldRecoverToDefaultTenant(errorMsg, currentTenant, defaultTenant) {
2504
- return /capability not enabled/i.test(errorMsg || "") && currentTenant !== defaultTenant;
2504
+ function isConsumerDisabled(errorMsg) {
2505
+ return /capability not enabled/i.test(errorMsg || "");
2505
2506
  }
2506
2507
  async function completeProviderSignIn(idToken, deps) {
2507
2508
  if (!idToken) return false;
@@ -2513,21 +2514,32 @@ async function completeProviderSignIn(idToken, deps) {
2513
2514
  // src/auth/LoginScreen.tsx
2514
2515
  import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
2515
2516
  var fontFam10 = Platform13.OS === "ios" ? "SF Pro Rounded" : void 0;
2516
- function LoginScreen({ activeTenant, defaultTenant, onConsumerDisabled, onSignedIn, onJoinTeam }) {
2517
+ function LoginScreen({ activeTenant, onPickTenant, onConsumerDisabled, onSignedIn, onJoinTeam }) {
2517
2518
  const [config, setConfig] = useState12(null);
2518
2519
  const [error, setError] = useState12(null);
2519
- useEffect11(() => {
2520
+ const [disabledTeam, setDisabledTeam] = useState12(null);
2521
+ const [askTeam, setAskTeam] = useState12(false);
2522
+ const askingTeam = !activeTenant || askTeam;
2523
+ const pick = async (tenant) => {
2524
+ setAskTeam(false);
2525
+ setDisabledTeam(null);
2526
+ await onPickTenant(tenant);
2527
+ };
2528
+ useEffect12(() => {
2520
2529
  setConfig(null);
2521
2530
  setError(null);
2531
+ if (!activeTenant) return;
2522
2532
  fetchConsumerConfig().then(setConfig).catch(async (err) => {
2523
2533
  const msg = err?.message || "Failed to load sign-in config";
2524
- if (shouldRecoverToDefaultTenant(msg, activeTenant, defaultTenant)) {
2534
+ if (isConsumerDisabled(msg)) {
2535
+ setDisabledTeam(activeTenant);
2536
+ setAskTeam(true);
2525
2537
  await onConsumerDisabled();
2526
2538
  return;
2527
2539
  }
2528
2540
  setError(msg);
2529
2541
  });
2530
- }, [activeTenant, defaultTenant, onConsumerDisabled]);
2542
+ }, [activeTenant, onConsumerDisabled]);
2531
2543
  return /* @__PURE__ */ jsxs16(View19, { style: styles18.container, children: [
2532
2544
  /* @__PURE__ */ jsxs16(View19, { style: styles18.hero, children: [
2533
2545
  /* @__PURE__ */ jsx20(AdaOrb6, { size: 140 }),
@@ -2535,20 +2547,51 @@ function LoginScreen({ activeTenant, defaultTenant, onConsumerDisabled, onSigned
2535
2547
  /* @__PURE__ */ jsx20(Text15, { style: styles18.tagline, children: "Your private personal AI-Team" })
2536
2548
  ] }),
2537
2549
  /* @__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 }),
2550
+ askingTeam && /* @__PURE__ */ jsx20(TeamField, { notice: disabledTeam, onPick: pick }),
2551
+ !askingTeam && !config && !error && /* @__PURE__ */ jsx20(ActivityIndicator9, { color: ada15.ink }),
2552
+ !askingTeam && !!error && /* @__PURE__ */ jsx20(Text15, { style: styles18.errorText, children: error }),
2553
+ !askingTeam && !!config && /* @__PURE__ */ jsx20(SignInButtons, { config, onSignedIn }),
2554
+ !askingTeam && /* @__PURE__ */ jsx20(Pressable12, { onPress: () => setAskTeam(true), hitSlop: 8, style: { paddingVertical: 4 }, children: /* @__PURE__ */ jsx20(Text15, { style: styles18.joinLink, children: "Use a different team" }) }),
2541
2555
  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
2556
  /* @__PURE__ */ jsx20(Text15, { style: styles18.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
2543
2557
  ] })
2544
2558
  ] });
2545
2559
  }
2560
+ function TeamField({ notice, onPick }) {
2561
+ const [team, setTeam] = useState12("");
2562
+ const slug = team.trim().toLowerCase();
2563
+ const submit = () => {
2564
+ if (slug) void onPick(slug);
2565
+ };
2566
+ return /* @__PURE__ */ jsxs16(Fragment5, { children: [
2567
+ !!notice && /* @__PURE__ */ jsxs16(Text15, { style: styles18.errorText, children: [
2568
+ "\u201C",
2569
+ notice,
2570
+ "\u201D doesn't have app sign-in. Try another team."
2571
+ ] }),
2572
+ /* @__PURE__ */ jsx20(
2573
+ TextInput3,
2574
+ {
2575
+ style: styles18.teamInput,
2576
+ value: team,
2577
+ onChangeText: setTeam,
2578
+ placeholder: "your team (e.g. acme)",
2579
+ placeholderTextColor: ada15.inkMuted,
2580
+ autoCapitalize: "none",
2581
+ autoCorrect: false,
2582
+ returnKeyType: "go",
2583
+ onSubmitEditing: submit
2584
+ }
2585
+ ),
2586
+ /* @__PURE__ */ jsx20(Pressable12, { style: [styles18.signInBtn, !slug && styles18.signInBtnDisabled], onPress: submit, disabled: !slug, children: /* @__PURE__ */ jsx20(Text15, { style: styles18.signInText, children: "Continue" }) })
2587
+ ] });
2588
+ }
2546
2589
  function SignInButtons({ config, onSignedIn }) {
2547
2590
  const [busy, setBusy] = useState12(false);
2548
2591
  const [error, setError] = useState12(null);
2549
2592
  const [appleAvailable, setAppleAvailable] = useState12(false);
2550
2593
  const { webClientId, iosClientId } = useMemo6(() => googleClientIds(config), [config]);
2551
- useEffect11(() => {
2594
+ useEffect12(() => {
2552
2595
  getOAuthProvider()?.isAppleAvailable().then(setAppleAvailable).catch(() => setAppleAvailable(false));
2553
2596
  }, []);
2554
2597
  const handleGoogleLogin = async () => {
@@ -2636,13 +2679,24 @@ var styles18 = StyleSheet18.create({
2636
2679
  signInGlyph: { fontSize: 18, fontWeight: "700", color: ada15.paper, fontFamily: fontFam10 },
2637
2680
  signInText: { fontSize: 16, fontWeight: "600", color: ada15.paper, letterSpacing: -0.2, fontFamily: fontFam10 },
2638
2681
  appleBtn: { width: "100%", height: 52 },
2682
+ teamInput: {
2683
+ width: "100%",
2684
+ backgroundColor: ada15.paperSoft,
2685
+ borderWidth: 1,
2686
+ borderColor: ada15.border,
2687
+ borderRadius: 14,
2688
+ paddingHorizontal: 16,
2689
+ paddingVertical: 14,
2690
+ fontSize: 16,
2691
+ color: ada15.ink
2692
+ },
2639
2693
  errorText: { fontSize: 13, color: "#c43d3d", textAlign: "center", paddingHorizontal: 16 },
2640
2694
  disclaimer: { fontSize: 12, color: ada15.inkMuted, textAlign: "center", marginTop: 8, lineHeight: 18, fontFamily: fontFam10 },
2641
2695
  joinLink: { fontSize: 14, fontWeight: "600", color: ada15.orangeDeep, textAlign: "center", fontFamily: fontFam10 }
2642
2696
  });
2643
2697
 
2644
2698
  // src/voice/VoiceCallSheet.tsx
2645
- import { useEffect as useEffect13, useRef as useRef6, useMemo as useMemo7 } from "react";
2699
+ import { useEffect as useEffect14, useRef as useRef6, useMemo as useMemo7 } from "react";
2646
2700
  import {
2647
2701
  View as View20,
2648
2702
  Text as Text16,
@@ -2656,7 +2710,7 @@ import { ada as ada16 } from "@ateam-ai-mobile/core";
2656
2710
  import { AdaOrb as AdaOrb7, SurfaceFullscreen as SurfaceFullscreen2 } from "@ateam-ai-mobile/plugin-runtime";
2657
2711
 
2658
2712
  // src/voice/useVoiceCall.ts
2659
- import { useCallback as useCallback6, useEffect as useEffect12, useReducer } from "react";
2713
+ import { useCallback as useCallback6, useEffect as useEffect13, useReducer } from "react";
2660
2714
 
2661
2715
  // src/voice/voiceSession.ts
2662
2716
  import { getVoiceApiUrl as getVoiceApiUrl2, getToken as getToken2, getTenant as getTenant5, getUser as getUser2, refreshToken } from "@ateam-ai-mobile/core";
@@ -2800,6 +2854,11 @@ var buildObserveScript = (text) => `
2800
2854
  }
2801
2855
  })(${JSON.stringify(text)}); true;
2802
2856
  `;
2857
+ var DEVICE_TOOL_ERROR_MAX = 200;
2858
+ function clipError(message) {
2859
+ const chars = Array.from(message);
2860
+ return chars.length > DEVICE_TOOL_ERROR_MAX ? chars.slice(0, DEVICE_TOOL_ERROR_MAX - 1).join("") + "\u2026" : message;
2861
+ }
2803
2862
  function originOf(url) {
2804
2863
  if (!url) return null;
2805
2864
  const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
@@ -2815,7 +2874,7 @@ var IDLE = {
2815
2874
  muted: false,
2816
2875
  userSpeaking: false,
2817
2876
  lastUserSpeechEndedAt: null,
2818
- lastDeviceTool: null,
2877
+ deviceToolCalls: [],
2819
2878
  mounted: false,
2820
2879
  webViewSource: null,
2821
2880
  skillSlug: void 0
@@ -2871,6 +2930,11 @@ var VoiceSessionManager = class {
2871
2930
  * and the bundleManager registers a plugin's tools under that same id (never
2872
2931
  * the bundle's own short `id`), so exact equality is the whole check. */
2873
2932
  this.toolScope = null;
2933
+ /** Bumped whenever a session begins or ends. A device-tool call remembers the
2934
+ * epoch it arrived in, and its outcome is recorded only if that session is
2935
+ * still the current one — a tool that finishes after hang-up (or after a new
2936
+ * call started) must not show up as the new session's last call. */
2937
+ this.sessionEpoch = 0;
2874
2938
  this.injectedTrackHook = INJECT_TRACK_HOOK;
2875
2939
  }
2876
2940
  /** The platform (built-in sheet, mic indicator, session host) sees and
@@ -2900,11 +2964,11 @@ var VoiceSessionManager = class {
2900
2964
  async start(opts, caller = PLATFORM_VOICE_CALLER) {
2901
2965
  if (this.state.callState === "connected") {
2902
2966
  if (!this.canAccess(caller)) throw this.foreignError();
2903
- return this.realSnapshot();
2967
+ return this.realSnapshot(caller);
2904
2968
  }
2905
2969
  if (this.state.callState === "connecting") {
2906
2970
  if (!this.canAccess(caller)) throw this.foreignError();
2907
- return this.waitForConnect();
2971
+ return this.waitForConnect(caller);
2908
2972
  }
2909
2973
  if (opts?.skills === "none" && opts?.skillSlug) {
2910
2974
  throw new Error("voice: skills:'none' (a clean call) cannot also hand the mic to a skill (skillSlug).");
@@ -2925,16 +2989,17 @@ var VoiceSessionManager = class {
2925
2989
  const after = this.getInternal().callState;
2926
2990
  if (after === "connected") {
2927
2991
  if (!this.canAccess(caller)) throw this.foreignError();
2928
- return this.realSnapshot();
2992
+ return this.realSnapshot(caller);
2929
2993
  }
2930
2994
  if (after === "connecting") {
2931
2995
  if (!this.canAccess(caller)) throw this.foreignError();
2932
- return this.waitForConnect();
2996
+ return this.waitForConnect(caller);
2933
2997
  }
2934
2998
  this.owner = caller;
2935
2999
  this.toolScope = opts?.skills === "none" && typeof caller === "string" ? caller : null;
2936
3000
  this.frozenTools = null;
2937
3001
  this.frozenSessionId = null;
3002
+ this.sessionEpoch++;
2938
3003
  const skillSlug = opts?.skillSlug;
2939
3004
  this.setState({
2940
3005
  callState: "connecting",
@@ -2943,12 +3008,15 @@ var VoiceSessionManager = class {
2943
3008
  aiPartial: "",
2944
3009
  userPartial: "",
2945
3010
  muted: false,
3011
+ // A session that ended from inside the voice page (not via stop()) left
3012
+ // its calls here; the new session starts with none.
3013
+ deviceToolCalls: [],
2946
3014
  mounted: true,
2947
3015
  skillSlug,
2948
3016
  persona: opts?.persona,
2949
3017
  webViewSource: { uri: this.buildUri(skillSlug, opts?.persona, opts?.skills) }
2950
3018
  });
2951
- return this.waitForConnect();
3019
+ return this.waitForConnect(caller);
2952
3020
  }
2953
3021
  stop(caller = PLATFORM_VOICE_CALLER) {
2954
3022
  if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
@@ -2962,6 +3030,7 @@ var VoiceSessionManager = class {
2962
3030
  this.rejectPending(new Error("voice: session stopped before it connected"));
2963
3031
  this.frozenTools = null;
2964
3032
  this.frozenSessionId = null;
3033
+ this.sessionEpoch++;
2965
3034
  this.setState({
2966
3035
  ...IDLE,
2967
3036
  // keep the transcript of the call that just ended for any reader still up
@@ -2994,9 +3063,11 @@ var VoiceSessionManager = class {
2994
3063
  this.webView?.injectJavaScript?.(buildObserveScript(text));
2995
3064
  }
2996
3065
  getSnapshot(caller = PLATFORM_VOICE_CALLER) {
2997
- return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
3066
+ return this.canAccess(caller) ? this.realSnapshot(caller) : this.foreignSnapshot();
2998
3067
  }
2999
- realSnapshot() {
3068
+ /** The snapshot for a caller that may access the session. Everything is the
3069
+ * session's, except `lastDeviceTool`, which is scoped to the caller's tools. */
3070
+ realSnapshot(caller) {
3000
3071
  const s = this.state;
3001
3072
  return {
3002
3073
  state: s.callState,
@@ -3006,11 +3077,23 @@ var VoiceSessionManager = class {
3006
3077
  listening: !!s.userPartial,
3007
3078
  userSpeaking: s.userSpeaking,
3008
3079
  lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
3009
- lastDeviceTool: s.lastDeviceTool,
3080
+ lastDeviceTool: this.lastDeviceToolFor(caller),
3010
3081
  authorizedSessionId: this.frozenSessionId,
3011
3082
  lastError: s.error
3012
3083
  };
3013
3084
  }
3085
+ /**
3086
+ * The last finished device-tool call THIS caller may see. The platform (the
3087
+ * user's own surface) sees every call. A plugin sees only calls to tools it
3088
+ * registered — even on a session it owns, the model can call another
3089
+ * solution's tool (a normal call offers every registered tool), and that
3090
+ * tool's name and error are not this plugin's to read.
3091
+ */
3092
+ lastDeviceToolFor(caller) {
3093
+ const calls = this.state.deviceToolCalls;
3094
+ if (caller === PLATFORM_VOICE_CALLER) return calls.length ? calls[calls.length - 1].call : null;
3095
+ return calls.find((r) => r.owner === caller)?.call ?? null;
3096
+ }
3014
3097
  foreignSnapshot() {
3015
3098
  return {
3016
3099
  state: "idle",
@@ -3138,18 +3221,52 @@ var VoiceSessionManager = class {
3138
3221
  console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
3139
3222
  break;
3140
3223
  }
3141
- if (data.name) this.setState({ lastDeviceTool: { name: data.name, at: Date.now() } });
3142
- if (!this.isToolAuthorized(data.name)) {
3143
- const notReady = this.frozenTools === null;
3144
- console.warn(`[voiceSession] REFUSED "${data.name}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
3145
- this.postDeviceToolRefusal(data.correlationId, data.name, notReady);
3146
- break;
3147
- }
3148
- void this.runDeviceTool(data);
3224
+ void this.handleDeviceToolCall(data);
3149
3225
  break;
3150
3226
  }
3151
3227
  }
3152
3228
  }
3229
+ /**
3230
+ * One device-tool call, arrival to outcome: validate → authorize → run. The
3231
+ * answer goes back to the mini-app (the model reads it) AND becomes
3232
+ * `lastDeviceTool` — the same object, recorded only once it is known, so a
3233
+ * refused call never looks like one that ran.
3234
+ */
3235
+ async handleDeviceToolCall(data) {
3236
+ const { correlationId, name, args, timeoutMs } = data || {};
3237
+ const at = Date.now();
3238
+ const epoch = this.sessionEpoch;
3239
+ const toolName = typeof name === "string" ? name : "";
3240
+ const sessionOwner = this.owner;
3241
+ const owner = typeof sessionOwner === "string" && toolName && hasDeviceTool(toolName, sessionOwner) ? sessionOwner : null;
3242
+ let answer;
3243
+ if (!toolName) {
3244
+ answer = { ok: false, reason: "invalid", error: "The device tool call named no tool." };
3245
+ } else if (!correlationId) {
3246
+ answer = {
3247
+ ok: false,
3248
+ reason: "invalid",
3249
+ error: `The call to "${toolName}" carried no correlationId, so it could not be answered.`
3250
+ };
3251
+ } else if (!this.isToolAuthorized(toolName)) {
3252
+ const notReady = this.frozenTools === null;
3253
+ console.warn(`[voiceSession] REFUSED "${toolName}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
3254
+ answer = notReady ? {
3255
+ ok: false,
3256
+ reason: "not_ready",
3257
+ error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
3258
+ } : { ok: false, reason: "not_authorized", error: `"${toolName}" is not available in this voice session.` };
3259
+ } else {
3260
+ answer = await this.runDeviceTool(toolName, args, timeoutMs);
3261
+ }
3262
+ if (correlationId) this.postDeviceToolResult({ correlationId, ...answer });
3263
+ if (epoch !== this.sessionEpoch) return;
3264
+ const ms = Math.max(0, Date.now() - at);
3265
+ const call = answer.ok ? { name: toolName, at, ms, ok: true, reason: null, error: null } : { name: toolName, at, ms, ok: false, reason: answer.reason, error: clipError(answer.error) };
3266
+ this.setState({
3267
+ deviceToolCalls: [...this.state.deviceToolCalls.filter((r) => r.owner !== owner), { owner, call }]
3268
+ });
3269
+ }
3153
3270
  /**
3154
3271
  * Final authorization = Core's frozen allowset ∩ the current valid local
3155
3272
  * registry, bound to the ACTIVE session. All must hold:
@@ -3169,21 +3286,9 @@ var VoiceSessionManager = class {
3169
3286
  if (!this.frozenTools.has(name)) return false;
3170
3287
  return hasDeviceTool(name, this.toolScope ?? void 0);
3171
3288
  }
3172
- /** Tell the mini-app the call was refused — a stated reason, never a silent drop.
3173
- * `notReady` distinguishes a transient "allowset not here yet" (retry) from a
3174
- * final "not allowed in this session". */
3175
- postDeviceToolRefusal(correlationId, name, notReady) {
3176
- const payload = notReady ? {
3177
- correlationId,
3178
- ok: false,
3179
- reason: "not_ready",
3180
- error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
3181
- } : {
3182
- correlationId,
3183
- ok: false,
3184
- reason: "not_authorized",
3185
- error: `"${name}" is not available in this voice session.`
3186
- };
3289
+ /** Answer the mini-app's pending device-tool request (a call's outcome, or the
3290
+ * tool list) — it resolves the request with this payload by correlationId. */
3291
+ postDeviceToolResult(payload) {
3187
3292
  this.webView?.injectJavaScript?.(
3188
3293
  `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3189
3294
  JSON.stringify(payload)
@@ -3200,28 +3305,16 @@ var VoiceSessionManager = class {
3200
3305
  description: t.description,
3201
3306
  input_schema: t.input_schema
3202
3307
  }));
3203
- this.webView?.injectJavaScript?.(
3204
- `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3205
- JSON.stringify({ correlationId, ok: true, tools })
3206
- )}); true;`
3207
- );
3308
+ this.postDeviceToolResult({ correlationId, ok: true, tools });
3208
3309
  }
3209
- /** Run a device tool and post the outcome back into the WebView. */
3210
- async runDeviceTool(data) {
3211
- const { correlationId, name, args, timeoutMs } = data || {};
3212
- if (!correlationId || typeof name !== "string") return;
3213
- let payload;
3310
+ /** Run an authorized device tool. Never throws: every failure is an answer. */
3311
+ async runDeviceTool(name, args, timeoutMs) {
3214
3312
  try {
3215
3313
  const outcome = await callDeviceTool(name, args || {}, { timeoutMs });
3216
- payload = outcome.ok ? { correlationId, ok: true, result: outcome.result } : { correlationId, ok: false, error: outcome.error, reason: outcome.reason };
3314
+ return outcome.ok ? { ok: true, result: outcome.result } : { ok: false, reason: outcome.reason, error: outcome.error };
3217
3315
  } catch (err) {
3218
- payload = { correlationId, ok: false, reason: "failed", error: err?.message || String(err) };
3316
+ return { ok: false, reason: "failed", error: err?.message || String(err) };
3219
3317
  }
3220
- this.webView?.injectJavaScript?.(
3221
- `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3222
- JSON.stringify(payload)
3223
- )}); true;`
3224
- );
3225
3318
  }
3226
3319
  // ── internals ────────────────────────────────────────────────
3227
3320
  setState(partial) {
@@ -3241,22 +3334,21 @@ var VoiceSessionManager = class {
3241
3334
  }
3242
3335
  }
3243
3336
  }
3244
- waitForConnect() {
3337
+ waitForConnect(caller) {
3245
3338
  return new Promise((resolve, reject) => {
3246
3339
  const timer = setTimeout(() => {
3247
3340
  this.pending = this.pending.filter((p) => p.timer !== timer);
3248
3341
  reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
3249
3342
  }, CONNECT_TIMEOUT_MS);
3250
- this.pending.push({ resolve, reject, timer });
3343
+ this.pending.push({ caller, resolve, reject, timer });
3251
3344
  });
3252
3345
  }
3253
3346
  resolvePending() {
3254
- const snap = this.realSnapshot();
3255
3347
  const waiting = this.pending;
3256
3348
  this.pending = [];
3257
3349
  for (const p of waiting) {
3258
3350
  clearTimeout(p.timer);
3259
- p.resolve(snap);
3351
+ p.resolve(this.realSnapshot(p.caller));
3260
3352
  }
3261
3353
  }
3262
3354
  rejectPending(err) {
@@ -3277,6 +3369,7 @@ var VoiceSessionManager = class {
3277
3369
  this.pendingStarts.clear();
3278
3370
  this.frozenTools = null;
3279
3371
  this.frozenSessionId = null;
3372
+ this.sessionEpoch++;
3280
3373
  this.listeners.clear();
3281
3374
  this.webView = null;
3282
3375
  }
@@ -3286,7 +3379,7 @@ var voiceSession = new VoiceSessionManager();
3286
3379
  // src/voice/useVoiceCall.ts
3287
3380
  function useVoiceCall({ skillSlug } = {}) {
3288
3381
  const [, force] = useReducer((x) => x + 1, 0);
3289
- useEffect12(() => {
3382
+ useEffect13(() => {
3290
3383
  const sub = voiceSession.subscribe(() => force());
3291
3384
  return () => sub.unsubscribe();
3292
3385
  }, []);
@@ -3375,10 +3468,10 @@ function VoiceCallSheet({ visible, onClose, skillSlug }) {
3375
3468
  toggleMute
3376
3469
  } = useVoiceCall({ skillSlug });
3377
3470
  const scrollRef = useRef6(null);
3378
- useEffect13(() => {
3471
+ useEffect14(() => {
3379
3472
  if (visible && callState === "idle") startCall();
3380
3473
  }, [visible]);
3381
- useEffect13(() => {
3474
+ useEffect14(() => {
3382
3475
  scrollRef.current?.scrollToEnd?.({ animated: true });
3383
3476
  }, [transcript.length, aiPartial, userPartial]);
3384
3477
  const handleHangUp = () => {
@@ -3571,14 +3664,14 @@ var styles19 = StyleSheet19.create({
3571
3664
  });
3572
3665
 
3573
3666
  // src/voice/VoiceSessionHost.tsx
3574
- import { useCallback as useCallback7, useEffect as useEffect14, useReducer as useReducer2, useRef as useRef7 } from "react";
3667
+ import { useCallback as useCallback7, useEffect as useEffect15, useReducer as useReducer2, useRef as useRef7 } from "react";
3575
3668
  import { View as View21, StyleSheet as StyleSheet20 } from "react-native";
3576
3669
  import { WebView as WebView3 } from "react-native-webview";
3577
3670
  import { jsx as jsx22 } from "react/jsx-runtime";
3578
3671
  function VoiceSessionHost() {
3579
3672
  const [, force] = useReducer2((x) => x + 1, 0);
3580
3673
  const lastSig = useRef7("");
3581
- useEffect14(() => {
3674
+ useEffect15(() => {
3582
3675
  const sub = voiceSession.subscribe(() => {
3583
3676
  const s2 = voiceSession.getInternal();
3584
3677
  const sig = `${s2.mounted}|${s2.webViewSource?.uri || ""}`;
@@ -3624,7 +3717,7 @@ var styles20 = StyleSheet20.create({
3624
3717
  });
3625
3718
 
3626
3719
  // src/voice/VoiceMicIndicator.tsx
3627
- import { useEffect as useEffect15, useReducer as useReducer3, useRef as useRef8 } from "react";
3720
+ import { useEffect as useEffect16, useReducer as useReducer3, useRef as useRef8 } from "react";
3628
3721
  import { View as View22, Text as Text17, Animated as Animated2, StyleSheet as StyleSheet21, Platform as Platform15 } from "react-native";
3629
3722
  import { ada as ada17 } from "@ateam-ai-mobile/core";
3630
3723
  import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
@@ -3632,13 +3725,13 @@ var fontFam12 = Platform15.OS === "ios" ? "SF Pro Rounded" : void 0;
3632
3725
  function VoiceMicIndicator({ topInset = 8 }) {
3633
3726
  const [, force] = useReducer3((x) => x + 1, 0);
3634
3727
  const pulse = useRef8(new Animated2.Value(0.4)).current;
3635
- useEffect15(() => {
3728
+ useEffect16(() => {
3636
3729
  const sub = voiceSession.subscribe(() => force());
3637
3730
  return () => sub.unsubscribe();
3638
3731
  }, []);
3639
3732
  const snap = voiceSession.getSnapshot();
3640
3733
  const live = snap.state === "connecting" || snap.state === "connected";
3641
- useEffect15(() => {
3734
+ useEffect16(() => {
3642
3735
  if (!live) return;
3643
3736
  const loop = Animated2.loop(
3644
3737
  Animated2.sequence([
@@ -3723,6 +3816,7 @@ export {
3723
3816
  googleClientIds,
3724
3817
  handoffErrorMessage,
3725
3818
  inviteHeadline,
3819
+ isConsumerDisabled,
3726
3820
  openJoinErrorMessage,
3727
3821
  openWidget,
3728
3822
  runHandoffExchange,
@@ -3730,7 +3824,6 @@ export {
3730
3824
  setOAuthProvider,
3731
3825
  setPluginWebViewExtraHosts,
3732
3826
  setUIActionNavigator,
3733
- shouldRecoverToDefaultTenant,
3734
3827
  useLightboxStore,
3735
3828
  usePluginBridge,
3736
3829
  useVoiceCall,