@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.js CHANGED
@@ -60,6 +60,7 @@ __export(index_exports, {
60
60
  googleClientIds: () => googleClientIds,
61
61
  handoffErrorMessage: () => handoffErrorMessage,
62
62
  inviteHeadline: () => inviteHeadline,
63
+ isConsumerDisabled: () => isConsumerDisabled,
63
64
  openJoinErrorMessage: () => openJoinErrorMessage,
64
65
  openWidget: () => openWidget,
65
66
  runHandoffExchange: () => runHandoffExchange,
@@ -67,7 +68,6 @@ __export(index_exports, {
67
68
  setOAuthProvider: () => setOAuthProvider,
68
69
  setPluginWebViewExtraHosts: () => setPluginWebViewExtraHosts,
69
70
  setUIActionNavigator: () => setUIActionNavigator,
70
- shouldRecoverToDefaultTenant: () => shouldRecoverToDefaultTenant,
71
71
  useLightboxStore: () => useLightboxStore,
72
72
  usePluginBridge: () => usePluginBridge,
73
73
  useVoiceCall: () => useVoiceCall,
@@ -651,6 +651,9 @@ function EmbeddedPluginWebView({ pluginId, manifest, commandBufferRef }) {
651
651
  const tenant = (0, import_core3.getTenant)();
652
652
  const { webViewRef, sendInit, handleMessage } = usePluginBridge(pluginId, manifest._connectorId, commandBufferRef);
653
653
  const url = (0, import_plugin_runtime.resolvePluginUrl)(manifest, tenant || void 0);
654
+ (0, import_react3.useEffect)(() => {
655
+ setError(null);
656
+ }, [url]);
654
657
  const bridgeJS = buildPluginBridgeJS(token || "", tenant || "");
655
658
  const computeAllowedHosts = (0, import_react3.useCallback)(() => {
656
659
  const hosts = /* @__PURE__ */ new Set();
@@ -2234,7 +2237,6 @@ var import_plugin_runtime9 = require("@ateam-ai-mobile/plugin-runtime");
2234
2237
  // src/auth/handoffExchange.ts
2235
2238
  async function runHandoffExchange(code, deps) {
2236
2239
  const res = await deps.exchange(code);
2237
- if (res.tenant) await deps.onAdoptTenant(res.tenant);
2238
2240
  await deps.onSignedIn(res.jwt);
2239
2241
  deps.onDone();
2240
2242
  }
@@ -2249,14 +2251,14 @@ function handoffErrorMessage(msg) {
2249
2251
  // src/auth/HandoffScreen.tsx
2250
2252
  var import_jsx_runtime17 = require("react/jsx-runtime");
2251
2253
  var fontFam7 = import_react_native16.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2252
- function HandoffScreen({ code, onAdoptTenant, onSignedIn, onDone, onCancel }) {
2254
+ function HandoffScreen({ code, onSignedIn, onDone, onCancel }) {
2253
2255
  const [err, setErr] = (0, import_react12.useState)(null);
2254
2256
  const startedForRef = (0, import_react12.useRef)(null);
2255
2257
  (0, import_react12.useEffect)(() => {
2256
2258
  if (!code || startedForRef.current === code) return;
2257
2259
  startedForRef.current = code;
2258
- runHandoffExchange(code, { exchange: import_core18.exchangeHandoff, onAdoptTenant, onSignedIn, onDone }).catch((e) => setErr(handoffErrorMessage(e?.message)));
2259
- }, [code, onAdoptTenant, onSignedIn, onDone]);
2260
+ runHandoffExchange(code, { exchange: import_core18.exchangeHandoff, onSignedIn, onDone }).catch((e) => setErr(handoffErrorMessage(e?.message)));
2261
+ }, [code, onSignedIn, onDone]);
2260
2262
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native16.View, { style: styles15.container, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native16.View, { style: styles15.center, children: [
2261
2263
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_plugin_runtime9.AdaOrb, { size: 72, state: err ? "idle" : "thinking" }),
2262
2264
  !err ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
@@ -2288,7 +2290,6 @@ var import_plugin_runtime10 = require("@ateam-ai-mobile/plugin-runtime");
2288
2290
  async function completeOpenJoin(slug, idToken, deps) {
2289
2291
  if (!idToken) return false;
2290
2292
  const consumer = await deps.join(slug, idToken);
2291
- await deps.onAdoptTenant(slug);
2292
2293
  await deps.onSignedIn(consumer.jwt);
2293
2294
  return true;
2294
2295
  }
@@ -2306,7 +2307,7 @@ function openJoinErrorMessage(msg) {
2306
2307
  // src/auth/OpenJoinScreen.tsx
2307
2308
  var import_jsx_runtime18 = require("react/jsx-runtime");
2308
2309
  var fontFam8 = import_react_native17.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2309
- function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }) {
2310
+ function OpenJoinScreen({ onSignedIn, onBack }) {
2310
2311
  const [team, setTeam] = (0, import_react13.useState)("");
2311
2312
  const [busy, setBusy] = (0, import_react13.useState)(false);
2312
2313
  const [err, setErr] = (0, import_react13.useState)(null);
@@ -2327,7 +2328,7 @@ function OpenJoinScreen({ onAdoptTenant, onSignedIn, onBack }) {
2327
2328
  setBusy(true);
2328
2329
  try {
2329
2330
  const idToken = await acquire(oauth);
2330
- await completeOpenJoin(slug, idToken, { join, onAdoptTenant, onSignedIn });
2331
+ await completeOpenJoin(slug, idToken, { join, onSignedIn });
2331
2332
  } catch (e) {
2332
2333
  setErr(openJoinErrorMessage(e?.message));
2333
2334
  } finally {
@@ -2535,8 +2536,8 @@ function googleClientIds(config) {
2535
2536
  iosClientId: g.ios_client_id || ""
2536
2537
  };
2537
2538
  }
2538
- function shouldRecoverToDefaultTenant(errorMsg, currentTenant, defaultTenant) {
2539
- return /capability not enabled/i.test(errorMsg || "") && currentTenant !== defaultTenant;
2539
+ function isConsumerDisabled(errorMsg) {
2540
+ return /capability not enabled/i.test(errorMsg || "");
2540
2541
  }
2541
2542
  async function completeProviderSignIn(idToken, deps) {
2542
2543
  if (!idToken) return false;
@@ -2548,21 +2549,32 @@ async function completeProviderSignIn(idToken, deps) {
2548
2549
  // src/auth/LoginScreen.tsx
2549
2550
  var import_jsx_runtime20 = require("react/jsx-runtime");
2550
2551
  var fontFam10 = import_react_native19.Platform.OS === "ios" ? "SF Pro Rounded" : void 0;
2551
- function LoginScreen({ activeTenant, defaultTenant, onConsumerDisabled, onSignedIn, onJoinTeam }) {
2552
+ function LoginScreen({ activeTenant, onPickTenant, onConsumerDisabled, onSignedIn, onJoinTeam }) {
2552
2553
  const [config, setConfig] = (0, import_react15.useState)(null);
2553
2554
  const [error, setError] = (0, import_react15.useState)(null);
2555
+ const [disabledTeam, setDisabledTeam] = (0, import_react15.useState)(null);
2556
+ const [askTeam, setAskTeam] = (0, import_react15.useState)(false);
2557
+ const askingTeam = !activeTenant || askTeam;
2558
+ const pick = async (tenant) => {
2559
+ setAskTeam(false);
2560
+ setDisabledTeam(null);
2561
+ await onPickTenant(tenant);
2562
+ };
2554
2563
  (0, import_react15.useEffect)(() => {
2555
2564
  setConfig(null);
2556
2565
  setError(null);
2566
+ if (!activeTenant) return;
2557
2567
  (0, import_core21.fetchConsumerConfig)().then(setConfig).catch(async (err) => {
2558
2568
  const msg = err?.message || "Failed to load sign-in config";
2559
- if (shouldRecoverToDefaultTenant(msg, activeTenant, defaultTenant)) {
2569
+ if (isConsumerDisabled(msg)) {
2570
+ setDisabledTeam(activeTenant);
2571
+ setAskTeam(true);
2560
2572
  await onConsumerDisabled();
2561
2573
  return;
2562
2574
  }
2563
2575
  setError(msg);
2564
2576
  });
2565
- }, [activeTenant, defaultTenant, onConsumerDisabled]);
2577
+ }, [activeTenant, onConsumerDisabled]);
2566
2578
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native19.View, { style: styles18.container, children: [
2567
2579
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native19.View, { style: styles18.hero, children: [
2568
2580
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_plugin_runtime12.AdaOrb, { size: 140 }),
@@ -2570,14 +2582,45 @@ function LoginScreen({ activeTenant, defaultTenant, onConsumerDisabled, onSigned
2570
2582
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.tagline, children: "Your private personal AI-Team" })
2571
2583
  ] }),
2572
2584
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native19.View, { style: styles18.buttonSection, children: [
2573
- !config && !error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.ActivityIndicator, { color: import_core21.ada.ink }),
2574
- !!error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.errorText, children: error }),
2575
- !!config && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SignInButtons, { config, onSignedIn }),
2585
+ askingTeam && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TeamField, { notice: disabledTeam, onPick: pick }),
2586
+ !askingTeam && !config && !error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.ActivityIndicator, { color: import_core21.ada.ink }),
2587
+ !askingTeam && !!error && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.errorText, children: error }),
2588
+ !askingTeam && !!config && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SignInButtons, { config, onSignedIn }),
2589
+ !askingTeam && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Pressable, { onPress: () => setAskTeam(true), hitSlop: 8, style: { paddingVertical: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.joinLink, children: "Use a different team" }) }),
2576
2590
  onJoinTeam && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Pressable, { onPress: onJoinTeam, hitSlop: 8, style: { paddingVertical: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.joinLink, children: "Join a team by name" }) }),
2577
2591
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.disclaimer, children: "By continuing you agree to Ada's Terms and Privacy Policy." })
2578
2592
  ] })
2579
2593
  ] });
2580
2594
  }
2595
+ function TeamField({ notice, onPick }) {
2596
+ const [team, setTeam] = (0, import_react15.useState)("");
2597
+ const slug = team.trim().toLowerCase();
2598
+ const submit = () => {
2599
+ if (slug) void onPick(slug);
2600
+ };
2601
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
2602
+ !!notice && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react_native19.Text, { style: styles18.errorText, children: [
2603
+ "\u201C",
2604
+ notice,
2605
+ "\u201D doesn't have app sign-in. Try another team."
2606
+ ] }),
2607
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2608
+ import_react_native19.TextInput,
2609
+ {
2610
+ style: styles18.teamInput,
2611
+ value: team,
2612
+ onChangeText: setTeam,
2613
+ placeholder: "your team (e.g. acme)",
2614
+ placeholderTextColor: import_core21.ada.inkMuted,
2615
+ autoCapitalize: "none",
2616
+ autoCorrect: false,
2617
+ returnKeyType: "go",
2618
+ onSubmitEditing: submit
2619
+ }
2620
+ ),
2621
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Pressable, { style: [styles18.signInBtn, !slug && styles18.signInBtnDisabled], onPress: submit, disabled: !slug, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_react_native19.Text, { style: styles18.signInText, children: "Continue" }) })
2622
+ ] });
2623
+ }
2581
2624
  function SignInButtons({ config, onSignedIn }) {
2582
2625
  const [busy, setBusy] = (0, import_react15.useState)(false);
2583
2626
  const [error, setError] = (0, import_react15.useState)(null);
@@ -2671,6 +2714,17 @@ var styles18 = import_react_native19.StyleSheet.create({
2671
2714
  signInGlyph: { fontSize: 18, fontWeight: "700", color: import_core21.ada.paper, fontFamily: fontFam10 },
2672
2715
  signInText: { fontSize: 16, fontWeight: "600", color: import_core21.ada.paper, letterSpacing: -0.2, fontFamily: fontFam10 },
2673
2716
  appleBtn: { width: "100%", height: 52 },
2717
+ teamInput: {
2718
+ width: "100%",
2719
+ backgroundColor: import_core21.ada.paperSoft,
2720
+ borderWidth: 1,
2721
+ borderColor: import_core21.ada.border,
2722
+ borderRadius: 14,
2723
+ paddingHorizontal: 16,
2724
+ paddingVertical: 14,
2725
+ fontSize: 16,
2726
+ color: import_core21.ada.ink
2727
+ },
2674
2728
  errorText: { fontSize: 13, color: "#c43d3d", textAlign: "center", paddingHorizontal: 16 },
2675
2729
  disclaimer: { fontSize: 12, color: import_core21.ada.inkMuted, textAlign: "center", marginTop: 8, lineHeight: 18, fontFamily: fontFam10 },
2676
2730
  joinLink: { fontSize: 14, fontWeight: "600", color: import_core21.ada.orangeDeep, textAlign: "center", fontFamily: fontFam10 }
@@ -2822,6 +2876,11 @@ var buildObserveScript = (text) => `
2822
2876
  }
2823
2877
  })(${JSON.stringify(text)}); true;
2824
2878
  `;
2879
+ var DEVICE_TOOL_ERROR_MAX = 200;
2880
+ function clipError(message) {
2881
+ const chars = Array.from(message);
2882
+ return chars.length > DEVICE_TOOL_ERROR_MAX ? chars.slice(0, DEVICE_TOOL_ERROR_MAX - 1).join("") + "\u2026" : message;
2883
+ }
2825
2884
  function originOf(url) {
2826
2885
  if (!url) return null;
2827
2886
  const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
@@ -2837,7 +2896,7 @@ var IDLE = {
2837
2896
  muted: false,
2838
2897
  userSpeaking: false,
2839
2898
  lastUserSpeechEndedAt: null,
2840
- lastDeviceTool: null,
2899
+ deviceToolCalls: [],
2841
2900
  mounted: false,
2842
2901
  webViewSource: null,
2843
2902
  skillSlug: void 0
@@ -2893,6 +2952,11 @@ var VoiceSessionManager = class {
2893
2952
  * and the bundleManager registers a plugin's tools under that same id (never
2894
2953
  * the bundle's own short `id`), so exact equality is the whole check. */
2895
2954
  this.toolScope = null;
2955
+ /** Bumped whenever a session begins or ends. A device-tool call remembers the
2956
+ * epoch it arrived in, and its outcome is recorded only if that session is
2957
+ * still the current one — a tool that finishes after hang-up (or after a new
2958
+ * call started) must not show up as the new session's last call. */
2959
+ this.sessionEpoch = 0;
2896
2960
  this.injectedTrackHook = INJECT_TRACK_HOOK;
2897
2961
  }
2898
2962
  /** The platform (built-in sheet, mic indicator, session host) sees and
@@ -2922,11 +2986,11 @@ var VoiceSessionManager = class {
2922
2986
  async start(opts, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2923
2987
  if (this.state.callState === "connected") {
2924
2988
  if (!this.canAccess(caller)) throw this.foreignError();
2925
- return this.realSnapshot();
2989
+ return this.realSnapshot(caller);
2926
2990
  }
2927
2991
  if (this.state.callState === "connecting") {
2928
2992
  if (!this.canAccess(caller)) throw this.foreignError();
2929
- return this.waitForConnect();
2993
+ return this.waitForConnect(caller);
2930
2994
  }
2931
2995
  if (opts?.skills === "none" && opts?.skillSlug) {
2932
2996
  throw new Error("voice: skills:'none' (a clean call) cannot also hand the mic to a skill (skillSlug).");
@@ -2947,16 +3011,17 @@ var VoiceSessionManager = class {
2947
3011
  const after = this.getInternal().callState;
2948
3012
  if (after === "connected") {
2949
3013
  if (!this.canAccess(caller)) throw this.foreignError();
2950
- return this.realSnapshot();
3014
+ return this.realSnapshot(caller);
2951
3015
  }
2952
3016
  if (after === "connecting") {
2953
3017
  if (!this.canAccess(caller)) throw this.foreignError();
2954
- return this.waitForConnect();
3018
+ return this.waitForConnect(caller);
2955
3019
  }
2956
3020
  this.owner = caller;
2957
3021
  this.toolScope = opts?.skills === "none" && typeof caller === "string" ? caller : null;
2958
3022
  this.frozenTools = null;
2959
3023
  this.frozenSessionId = null;
3024
+ this.sessionEpoch++;
2960
3025
  const skillSlug = opts?.skillSlug;
2961
3026
  this.setState({
2962
3027
  callState: "connecting",
@@ -2965,12 +3030,15 @@ var VoiceSessionManager = class {
2965
3030
  aiPartial: "",
2966
3031
  userPartial: "",
2967
3032
  muted: false,
3033
+ // A session that ended from inside the voice page (not via stop()) left
3034
+ // its calls here; the new session starts with none.
3035
+ deviceToolCalls: [],
2968
3036
  mounted: true,
2969
3037
  skillSlug,
2970
3038
  persona: opts?.persona,
2971
3039
  webViewSource: { uri: this.buildUri(skillSlug, opts?.persona, opts?.skills) }
2972
3040
  });
2973
- return this.waitForConnect();
3041
+ return this.waitForConnect(caller);
2974
3042
  }
2975
3043
  stop(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
2976
3044
  if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
@@ -2984,6 +3052,7 @@ var VoiceSessionManager = class {
2984
3052
  this.rejectPending(new Error("voice: session stopped before it connected"));
2985
3053
  this.frozenTools = null;
2986
3054
  this.frozenSessionId = null;
3055
+ this.sessionEpoch++;
2987
3056
  this.setState({
2988
3057
  ...IDLE,
2989
3058
  // keep the transcript of the call that just ended for any reader still up
@@ -3016,9 +3085,11 @@ var VoiceSessionManager = class {
3016
3085
  this.webView?.injectJavaScript?.(buildObserveScript(text));
3017
3086
  }
3018
3087
  getSnapshot(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
3019
- return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
3088
+ return this.canAccess(caller) ? this.realSnapshot(caller) : this.foreignSnapshot();
3020
3089
  }
3021
- realSnapshot() {
3090
+ /** The snapshot for a caller that may access the session. Everything is the
3091
+ * session's, except `lastDeviceTool`, which is scoped to the caller's tools. */
3092
+ realSnapshot(caller) {
3022
3093
  const s = this.state;
3023
3094
  return {
3024
3095
  state: s.callState,
@@ -3028,11 +3099,23 @@ var VoiceSessionManager = class {
3028
3099
  listening: !!s.userPartial,
3029
3100
  userSpeaking: s.userSpeaking,
3030
3101
  lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
3031
- lastDeviceTool: s.lastDeviceTool,
3102
+ lastDeviceTool: this.lastDeviceToolFor(caller),
3032
3103
  authorizedSessionId: this.frozenSessionId,
3033
3104
  lastError: s.error
3034
3105
  };
3035
3106
  }
3107
+ /**
3108
+ * The last finished device-tool call THIS caller may see. The platform (the
3109
+ * user's own surface) sees every call. A plugin sees only calls to tools it
3110
+ * registered — even on a session it owns, the model can call another
3111
+ * solution's tool (a normal call offers every registered tool), and that
3112
+ * tool's name and error are not this plugin's to read.
3113
+ */
3114
+ lastDeviceToolFor(caller) {
3115
+ const calls = this.state.deviceToolCalls;
3116
+ if (caller === import_plugin_runtime13.PLATFORM_VOICE_CALLER) return calls.length ? calls[calls.length - 1].call : null;
3117
+ return calls.find((r) => r.owner === caller)?.call ?? null;
3118
+ }
3036
3119
  foreignSnapshot() {
3037
3120
  return {
3038
3121
  state: "idle",
@@ -3160,18 +3243,52 @@ var VoiceSessionManager = class {
3160
3243
  console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
3161
3244
  break;
3162
3245
  }
3163
- if (data.name) this.setState({ lastDeviceTool: { name: data.name, at: Date.now() } });
3164
- if (!this.isToolAuthorized(data.name)) {
3165
- const notReady = this.frozenTools === null;
3166
- console.warn(`[voiceSession] REFUSED "${data.name}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
3167
- this.postDeviceToolRefusal(data.correlationId, data.name, notReady);
3168
- break;
3169
- }
3170
- void this.runDeviceTool(data);
3246
+ void this.handleDeviceToolCall(data);
3171
3247
  break;
3172
3248
  }
3173
3249
  }
3174
3250
  }
3251
+ /**
3252
+ * One device-tool call, arrival to outcome: validate → authorize → run. The
3253
+ * answer goes back to the mini-app (the model reads it) AND becomes
3254
+ * `lastDeviceTool` — the same object, recorded only once it is known, so a
3255
+ * refused call never looks like one that ran.
3256
+ */
3257
+ async handleDeviceToolCall(data) {
3258
+ const { correlationId, name, args, timeoutMs } = data || {};
3259
+ const at = Date.now();
3260
+ const epoch = this.sessionEpoch;
3261
+ const toolName = typeof name === "string" ? name : "";
3262
+ const sessionOwner = this.owner;
3263
+ const owner = typeof sessionOwner === "string" && toolName && (0, import_plugin_runtime13.hasDeviceTool)(toolName, sessionOwner) ? sessionOwner : null;
3264
+ let answer;
3265
+ if (!toolName) {
3266
+ answer = { ok: false, reason: "invalid", error: "The device tool call named no tool." };
3267
+ } else if (!correlationId) {
3268
+ answer = {
3269
+ ok: false,
3270
+ reason: "invalid",
3271
+ error: `The call to "${toolName}" carried no correlationId, so it could not be answered.`
3272
+ };
3273
+ } else if (!this.isToolAuthorized(toolName)) {
3274
+ const notReady = this.frozenTools === null;
3275
+ console.warn(`[voiceSession] REFUSED "${toolName}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
3276
+ answer = notReady ? {
3277
+ ok: false,
3278
+ reason: "not_ready",
3279
+ error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
3280
+ } : { ok: false, reason: "not_authorized", error: `"${toolName}" is not available in this voice session.` };
3281
+ } else {
3282
+ answer = await this.runDeviceTool(toolName, args, timeoutMs);
3283
+ }
3284
+ if (correlationId) this.postDeviceToolResult({ correlationId, ...answer });
3285
+ if (epoch !== this.sessionEpoch) return;
3286
+ const ms = Math.max(0, Date.now() - at);
3287
+ 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) };
3288
+ this.setState({
3289
+ deviceToolCalls: [...this.state.deviceToolCalls.filter((r) => r.owner !== owner), { owner, call }]
3290
+ });
3291
+ }
3175
3292
  /**
3176
3293
  * Final authorization = Core's frozen allowset ∩ the current valid local
3177
3294
  * registry, bound to the ACTIVE session. All must hold:
@@ -3191,21 +3308,9 @@ var VoiceSessionManager = class {
3191
3308
  if (!this.frozenTools.has(name)) return false;
3192
3309
  return (0, import_plugin_runtime13.hasDeviceTool)(name, this.toolScope ?? void 0);
3193
3310
  }
3194
- /** Tell the mini-app the call was refused — a stated reason, never a silent drop.
3195
- * `notReady` distinguishes a transient "allowset not here yet" (retry) from a
3196
- * final "not allowed in this session". */
3197
- postDeviceToolRefusal(correlationId, name, notReady) {
3198
- const payload = notReady ? {
3199
- correlationId,
3200
- ok: false,
3201
- reason: "not_ready",
3202
- error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
3203
- } : {
3204
- correlationId,
3205
- ok: false,
3206
- reason: "not_authorized",
3207
- error: `"${name}" is not available in this voice session.`
3208
- };
3311
+ /** Answer the mini-app's pending device-tool request (a call's outcome, or the
3312
+ * tool list) — it resolves the request with this payload by correlationId. */
3313
+ postDeviceToolResult(payload) {
3209
3314
  this.webView?.injectJavaScript?.(
3210
3315
  `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3211
3316
  JSON.stringify(payload)
@@ -3222,28 +3327,16 @@ var VoiceSessionManager = class {
3222
3327
  description: t.description,
3223
3328
  input_schema: t.input_schema
3224
3329
  }));
3225
- this.webView?.injectJavaScript?.(
3226
- `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3227
- JSON.stringify({ correlationId, ok: true, tools })
3228
- )}); true;`
3229
- );
3330
+ this.postDeviceToolResult({ correlationId, ok: true, tools });
3230
3331
  }
3231
- /** Run a device tool and post the outcome back into the WebView. */
3232
- async runDeviceTool(data) {
3233
- const { correlationId, name, args, timeoutMs } = data || {};
3234
- if (!correlationId || typeof name !== "string") return;
3235
- let payload;
3332
+ /** Run an authorized device tool. Never throws: every failure is an answer. */
3333
+ async runDeviceTool(name, args, timeoutMs) {
3236
3334
  try {
3237
3335
  const outcome = await (0, import_plugin_runtime13.callDeviceTool)(name, args || {}, { timeoutMs });
3238
- payload = outcome.ok ? { correlationId, ok: true, result: outcome.result } : { correlationId, ok: false, error: outcome.error, reason: outcome.reason };
3336
+ return outcome.ok ? { ok: true, result: outcome.result } : { ok: false, reason: outcome.reason, error: outcome.error };
3239
3337
  } catch (err) {
3240
- payload = { correlationId, ok: false, reason: "failed", error: err?.message || String(err) };
3338
+ return { ok: false, reason: "failed", error: err?.message || String(err) };
3241
3339
  }
3242
- this.webView?.injectJavaScript?.(
3243
- `window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
3244
- JSON.stringify(payload)
3245
- )}); true;`
3246
- );
3247
3340
  }
3248
3341
  // ── internals ────────────────────────────────────────────────
3249
3342
  setState(partial) {
@@ -3263,22 +3356,21 @@ var VoiceSessionManager = class {
3263
3356
  }
3264
3357
  }
3265
3358
  }
3266
- waitForConnect() {
3359
+ waitForConnect(caller) {
3267
3360
  return new Promise((resolve, reject) => {
3268
3361
  const timer = setTimeout(() => {
3269
3362
  this.pending = this.pending.filter((p) => p.timer !== timer);
3270
3363
  reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
3271
3364
  }, CONNECT_TIMEOUT_MS);
3272
- this.pending.push({ resolve, reject, timer });
3365
+ this.pending.push({ caller, resolve, reject, timer });
3273
3366
  });
3274
3367
  }
3275
3368
  resolvePending() {
3276
- const snap = this.realSnapshot();
3277
3369
  const waiting = this.pending;
3278
3370
  this.pending = [];
3279
3371
  for (const p of waiting) {
3280
3372
  clearTimeout(p.timer);
3281
- p.resolve(snap);
3373
+ p.resolve(this.realSnapshot(p.caller));
3282
3374
  }
3283
3375
  }
3284
3376
  rejectPending(err) {
@@ -3299,6 +3391,7 @@ var VoiceSessionManager = class {
3299
3391
  this.pendingStarts.clear();
3300
3392
  this.frozenTools = null;
3301
3393
  this.frozenSessionId = null;
3394
+ this.sessionEpoch++;
3302
3395
  this.listeners.clear();
3303
3396
  this.webView = null;
3304
3397
  }
@@ -3746,6 +3839,7 @@ var styles21 = import_react_native22.StyleSheet.create({
3746
3839
  googleClientIds,
3747
3840
  handoffErrorMessage,
3748
3841
  inviteHeadline,
3842
+ isConsumerDisabled,
3749
3843
  openJoinErrorMessage,
3750
3844
  openWidget,
3751
3845
  runHandoffExchange,
@@ -3753,7 +3847,6 @@ var styles21 = import_react_native22.StyleSheet.create({
3753
3847
  setOAuthProvider,
3754
3848
  setPluginWebViewExtraHosts,
3755
3849
  setUIActionNavigator,
3756
- shouldRecoverToDefaultTenant,
3757
3850
  useLightboxStore,
3758
3851
  usePluginBridge,
3759
3852
  useVoiceCall,