@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.d.mts +63 -47
- package/dist/index.d.ts +63 -47
- package/dist/index.js +163 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +195 -102
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/auth/HandoffScreen.tsx +3 -11
- package/src/auth/LoginScreen.tsx +68 -17
- package/src/auth/OpenJoinScreen.tsx +4 -6
- package/src/auth/handoffExchange.ts +6 -9
- package/src/auth/login.ts +5 -10
- package/src/auth/openJoin.ts +5 -8
- package/src/index.ts +1 -1
- package/src/plugin/EmbeddedPluginWebView.tsx +4 -1
- package/src/voice/voiceSession.ts +150 -64
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,
|
|
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,
|
|
2259
|
-
}, [code,
|
|
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({
|
|
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,
|
|
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
|
|
2539
|
-
return /capability not enabled/i.test(errorMsg || "")
|
|
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,
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
2574
|
-
|
|
2575
|
-
!!
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
3195
|
-
*
|
|
3196
|
-
|
|
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.
|
|
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
|
|
3232
|
-
async runDeviceTool(
|
|
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
|
-
|
|
3336
|
+
return outcome.ok ? { ok: true, result: outcome.result } : { ok: false, reason: outcome.reason, error: outcome.error };
|
|
3239
3337
|
} catch (err) {
|
|
3240
|
-
|
|
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(
|
|
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,
|