@ateam-ai-mobile/ui 0.1.7 → 0.1.9
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 +95 -7
- package/dist/index.d.ts +95 -7
- package/dist/index.js +219 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +220 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/plugin/Plugin.tsx +4 -3
- package/src/voice/voiceSession.ts +307 -42
package/dist/index.js
CHANGED
|
@@ -2822,6 +2822,11 @@ var buildObserveScript = (text) => `
|
|
|
2822
2822
|
}
|
|
2823
2823
|
})(${JSON.stringify(text)}); true;
|
|
2824
2824
|
`;
|
|
2825
|
+
var DEVICE_TOOL_ERROR_MAX = 200;
|
|
2826
|
+
function clipError(message) {
|
|
2827
|
+
const chars = Array.from(message);
|
|
2828
|
+
return chars.length > DEVICE_TOOL_ERROR_MAX ? chars.slice(0, DEVICE_TOOL_ERROR_MAX - 1).join("") + "\u2026" : message;
|
|
2829
|
+
}
|
|
2825
2830
|
function originOf(url) {
|
|
2826
2831
|
if (!url) return null;
|
|
2827
2832
|
const m = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)/i.exec(url);
|
|
@@ -2837,6 +2842,7 @@ var IDLE = {
|
|
|
2837
2842
|
muted: false,
|
|
2838
2843
|
userSpeaking: false,
|
|
2839
2844
|
lastUserSpeechEndedAt: null,
|
|
2845
|
+
deviceToolCalls: [],
|
|
2840
2846
|
mounted: false,
|
|
2841
2847
|
webViewSource: null,
|
|
2842
2848
|
skillSlug: void 0
|
|
@@ -2858,6 +2864,45 @@ var VoiceSessionManager = class {
|
|
|
2858
2864
|
* conversation just because it declared voice.
|
|
2859
2865
|
*/
|
|
2860
2866
|
this.owner = null;
|
|
2867
|
+
/** EVERY start() still in its pre-mount awaits (tool preload, token refresh).
|
|
2868
|
+
* The session is not 'connecting' yet, so a stop() arriving now would otherwise
|
|
2869
|
+
* be an idle no-op — and a start would then mount a call for a screen that has
|
|
2870
|
+
* already gone. A set, not one slot: two concurrent starts must BOTH be
|
|
2871
|
+
* cancellable. stop() cancels every pending start of its caller (the platform:
|
|
2872
|
+
* all of them); another plugin's stop cancels none. */
|
|
2873
|
+
this.pendingStarts = /* @__PURE__ */ new Set();
|
|
2874
|
+
/**
|
|
2875
|
+
* The device-tool names Core ALLOWED for this voice session — Core's sanitized
|
|
2876
|
+
* allowset, delivered to the phone in a `deviceToolAllowset` message.
|
|
2877
|
+
* Authorization gates against THIS (necessary, alongside the registry's can-do).
|
|
2878
|
+
*
|
|
2879
|
+
* WHY (GPT gate 1, corrected): the phone only PROPOSES its available tools
|
|
2880
|
+
* (sendDeviceToolList); CORE is the session authority and decides which are
|
|
2881
|
+
* accepted (session.device_tools). Freezing the phone's own proposal would let
|
|
2882
|
+
* a tool Core REJECTED still run locally. So the authz set is populated from
|
|
2883
|
+
* Core's accepted set, never from listDeviceTools(). Until Core's set arrives
|
|
2884
|
+
* this is null and every call FAILS CLOSED. Pinned once per session; the live
|
|
2885
|
+
* registry (which drifts via lazy loads / host registration / tenant changes)
|
|
2886
|
+
* never feeds it.
|
|
2887
|
+
*/
|
|
2888
|
+
this.frozenTools = null;
|
|
2889
|
+
/** The voice `sessionId` the current allowset is BOUND to. Authorization
|
|
2890
|
+
* requires it to equal the active session's id (state.sessionId), so a stale
|
|
2891
|
+
* allowset from a previous/other session is refused. */
|
|
2892
|
+
this.frozenSessionId = null;
|
|
2893
|
+
/** A CLEAN call (skills:'none') started by a plugin offers and runs ONLY that
|
|
2894
|
+
* plugin's own device tools — not other solutions' tools that happen to be
|
|
2895
|
+
* registered on the same phone. null = every registered tool (a normal call,
|
|
2896
|
+
* or the platform's). Set by every start(); like `owner`, kept after stop.
|
|
2897
|
+
* ONE identity: the caller is the TRUSTED manifest pluginId gateVoice stamps,
|
|
2898
|
+
* and the bundleManager registers a plugin's tools under that same id (never
|
|
2899
|
+
* the bundle's own short `id`), so exact equality is the whole check. */
|
|
2900
|
+
this.toolScope = null;
|
|
2901
|
+
/** Bumped whenever a session begins or ends. A device-tool call remembers the
|
|
2902
|
+
* epoch it arrived in, and its outcome is recorded only if that session is
|
|
2903
|
+
* still the current one — a tool that finishes after hang-up (or after a new
|
|
2904
|
+
* call started) must not show up as the new session's last call. */
|
|
2905
|
+
this.sessionEpoch = 0;
|
|
2861
2906
|
this.injectedTrackHook = INJECT_TRACK_HOOK;
|
|
2862
2907
|
}
|
|
2863
2908
|
/** The platform (built-in sheet, mic indicator, session host) sees and
|
|
@@ -2887,12 +2932,17 @@ var VoiceSessionManager = class {
|
|
|
2887
2932
|
async start(opts, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
|
|
2888
2933
|
if (this.state.callState === "connected") {
|
|
2889
2934
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2890
|
-
return this.realSnapshot();
|
|
2935
|
+
return this.realSnapshot(caller);
|
|
2891
2936
|
}
|
|
2892
2937
|
if (this.state.callState === "connecting") {
|
|
2893
2938
|
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2894
|
-
return this.waitForConnect();
|
|
2939
|
+
return this.waitForConnect(caller);
|
|
2940
|
+
}
|
|
2941
|
+
if (opts?.skills === "none" && opts?.skillSlug) {
|
|
2942
|
+
throw new Error("voice: skills:'none' (a clean call) cannot also hand the mic to a skill (skillSlug).");
|
|
2895
2943
|
}
|
|
2944
|
+
const ticket = { caller, cancelled: false };
|
|
2945
|
+
this.pendingStarts.add(ticket);
|
|
2896
2946
|
try {
|
|
2897
2947
|
await (0, import_plugin_runtime13.ensureDeviceToolsLoaded)();
|
|
2898
2948
|
} catch (e) {
|
|
@@ -2902,7 +2952,22 @@ var VoiceSessionManager = class {
|
|
|
2902
2952
|
if (tokenNeedsRefresh((0, import_core22.getToken)())) await (0, import_core22.refreshToken)();
|
|
2903
2953
|
} catch {
|
|
2904
2954
|
}
|
|
2955
|
+
this.pendingStarts.delete(ticket);
|
|
2956
|
+
if (ticket.cancelled) throw new Error("voice: stopped before the session started");
|
|
2957
|
+
const after = this.getInternal().callState;
|
|
2958
|
+
if (after === "connected") {
|
|
2959
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2960
|
+
return this.realSnapshot(caller);
|
|
2961
|
+
}
|
|
2962
|
+
if (after === "connecting") {
|
|
2963
|
+
if (!this.canAccess(caller)) throw this.foreignError();
|
|
2964
|
+
return this.waitForConnect(caller);
|
|
2965
|
+
}
|
|
2905
2966
|
this.owner = caller;
|
|
2967
|
+
this.toolScope = opts?.skills === "none" && typeof caller === "string" ? caller : null;
|
|
2968
|
+
this.frozenTools = null;
|
|
2969
|
+
this.frozenSessionId = null;
|
|
2970
|
+
this.sessionEpoch++;
|
|
2906
2971
|
const skillSlug = opts?.skillSlug;
|
|
2907
2972
|
this.setState({
|
|
2908
2973
|
callState: "connecting",
|
|
@@ -2911,16 +2976,29 @@ var VoiceSessionManager = class {
|
|
|
2911
2976
|
aiPartial: "",
|
|
2912
2977
|
userPartial: "",
|
|
2913
2978
|
muted: false,
|
|
2979
|
+
// A session that ended from inside the voice page (not via stop()) left
|
|
2980
|
+
// its calls here; the new session starts with none.
|
|
2981
|
+
deviceToolCalls: [],
|
|
2914
2982
|
mounted: true,
|
|
2915
2983
|
skillSlug,
|
|
2916
|
-
|
|
2984
|
+
persona: opts?.persona,
|
|
2985
|
+
webViewSource: { uri: this.buildUri(skillSlug, opts?.persona, opts?.skills) }
|
|
2917
2986
|
});
|
|
2918
|
-
return this.waitForConnect();
|
|
2987
|
+
return this.waitForConnect(caller);
|
|
2919
2988
|
}
|
|
2920
2989
|
stop(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
|
|
2921
2990
|
if (this.state.callState !== "idle" && !this.canAccess(caller)) throw this.foreignError();
|
|
2991
|
+
for (const t of [...this.pendingStarts]) {
|
|
2992
|
+
if (caller === import_plugin_runtime13.PLATFORM_VOICE_CALLER || caller === t.caller) {
|
|
2993
|
+
t.cancelled = true;
|
|
2994
|
+
this.pendingStarts.delete(t);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2922
2997
|
this.webView?.injectJavaScript?.("window.hangUp && window.hangUp(); true;");
|
|
2923
2998
|
this.rejectPending(new Error("voice: session stopped before it connected"));
|
|
2999
|
+
this.frozenTools = null;
|
|
3000
|
+
this.frozenSessionId = null;
|
|
3001
|
+
this.sessionEpoch++;
|
|
2924
3002
|
this.setState({
|
|
2925
3003
|
...IDLE,
|
|
2926
3004
|
// keep the transcript of the call that just ended for any reader still up
|
|
@@ -2941,10 +3019,9 @@ var VoiceSessionManager = class {
|
|
|
2941
3019
|
}
|
|
2942
3020
|
/**
|
|
2943
3021
|
* Stream a silent observation into the live session — tell the model without
|
|
2944
|
-
* asking it to speak. The cheap
|
|
2945
|
-
*
|
|
2946
|
-
*
|
|
2947
|
-
* plugin API is added. Owner-scoped like every other control.
|
|
3022
|
+
* asking it to speak. The cheap channel behind the gated props.voice.observe();
|
|
3023
|
+
* every item stays in the model's context for the call, so a caller sends only
|
|
3024
|
+
* material changes. Owner-scoped like every other control.
|
|
2948
3025
|
*/
|
|
2949
3026
|
observe(text, caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
|
|
2950
3027
|
if (this.state.callState !== "connected") {
|
|
@@ -2954,9 +3031,11 @@ var VoiceSessionManager = class {
|
|
|
2954
3031
|
this.webView?.injectJavaScript?.(buildObserveScript(text));
|
|
2955
3032
|
}
|
|
2956
3033
|
getSnapshot(caller = import_plugin_runtime13.PLATFORM_VOICE_CALLER) {
|
|
2957
|
-
return this.canAccess(caller) ? this.realSnapshot() : this.foreignSnapshot();
|
|
3034
|
+
return this.canAccess(caller) ? this.realSnapshot(caller) : this.foreignSnapshot();
|
|
2958
3035
|
}
|
|
2959
|
-
|
|
3036
|
+
/** The snapshot for a caller that may access the session. Everything is the
|
|
3037
|
+
* session's, except `lastDeviceTool`, which is scoped to the caller's tools. */
|
|
3038
|
+
realSnapshot(caller) {
|
|
2960
3039
|
const s = this.state;
|
|
2961
3040
|
return {
|
|
2962
3041
|
state: s.callState,
|
|
@@ -2966,9 +3045,23 @@ var VoiceSessionManager = class {
|
|
|
2966
3045
|
listening: !!s.userPartial,
|
|
2967
3046
|
userSpeaking: s.userSpeaking,
|
|
2968
3047
|
lastUserSpeechEndedAt: s.lastUserSpeechEndedAt,
|
|
3048
|
+
lastDeviceTool: this.lastDeviceToolFor(caller),
|
|
3049
|
+
authorizedSessionId: this.frozenSessionId,
|
|
2969
3050
|
lastError: s.error
|
|
2970
3051
|
};
|
|
2971
3052
|
}
|
|
3053
|
+
/**
|
|
3054
|
+
* The last finished device-tool call THIS caller may see. The platform (the
|
|
3055
|
+
* user's own surface) sees every call. A plugin sees only calls to tools it
|
|
3056
|
+
* registered — even on a session it owns, the model can call another
|
|
3057
|
+
* solution's tool (a normal call offers every registered tool), and that
|
|
3058
|
+
* tool's name and error are not this plugin's to read.
|
|
3059
|
+
*/
|
|
3060
|
+
lastDeviceToolFor(caller) {
|
|
3061
|
+
const calls = this.state.deviceToolCalls;
|
|
3062
|
+
if (caller === import_plugin_runtime13.PLATFORM_VOICE_CALLER) return calls.length ? calls[calls.length - 1].call : null;
|
|
3063
|
+
return calls.find((r) => r.owner === caller)?.call ?? null;
|
|
3064
|
+
}
|
|
2972
3065
|
foreignSnapshot() {
|
|
2973
3066
|
return {
|
|
2974
3067
|
state: "idle",
|
|
@@ -2978,6 +3071,8 @@ var VoiceSessionManager = class {
|
|
|
2978
3071
|
listening: false,
|
|
2979
3072
|
userSpeaking: false,
|
|
2980
3073
|
lastUserSpeechEndedAt: null,
|
|
3074
|
+
lastDeviceTool: null,
|
|
3075
|
+
authorizedSessionId: null,
|
|
2981
3076
|
lastError: null
|
|
2982
3077
|
};
|
|
2983
3078
|
}
|
|
@@ -2994,12 +3089,15 @@ var VoiceSessionManager = class {
|
|
|
2994
3089
|
return this.canAccess(caller) ? this.state.transcript.slice() : [];
|
|
2995
3090
|
}
|
|
2996
3091
|
// ── WebView plumbing ─────────────────────────────────────────
|
|
2997
|
-
buildUri(skillSlug) {
|
|
3092
|
+
buildUri(skillSlug, persona, skills) {
|
|
2998
3093
|
const token = (0, import_core22.getToken)();
|
|
2999
3094
|
const tenant = (0, import_core22.getTenant)();
|
|
3000
3095
|
const actorId = (0, import_core22.getUser)()?.actorId || "";
|
|
3001
3096
|
const skillParam = skillSlug ? `&skillSlug=${encodeURIComponent(skillSlug)}` : "";
|
|
3002
|
-
|
|
3097
|
+
const p = persona && (persona.name || persona.style) ? { ...persona.name ? { name: persona.name } : {}, ...persona.style ? { style: persona.style } : {} } : null;
|
|
3098
|
+
const personaParam = p ? `&persona=${encodeURIComponent(JSON.stringify(p))}` : "";
|
|
3099
|
+
const skillsParam = skills === "none" ? "&skills=none" : "";
|
|
3100
|
+
return `${(0, import_core22.getVoiceApiUrl)()}/app?mode=mini&embedded=rn&authToken=${encodeURIComponent(token || "")}&tenant=${encodeURIComponent(tenant)}&actorId=${encodeURIComponent(actorId)}${skillParam}${personaParam}${skillsParam}`;
|
|
3003
3101
|
}
|
|
3004
3102
|
/** Handle a postMessage from the WebView. `event` is the RN WebView event. */
|
|
3005
3103
|
handleMessage(event) {
|
|
@@ -3063,6 +3161,27 @@ var VoiceSessionManager = class {
|
|
|
3063
3161
|
this.sendDeviceToolList(data.correlationId);
|
|
3064
3162
|
break;
|
|
3065
3163
|
}
|
|
3164
|
+
// CORE's ALLOWSET for this session — what the model may ASK for. Core sends
|
|
3165
|
+
// it (even empty, on purpose) at session start. It is a NECESSARY gate, not
|
|
3166
|
+
// the whole story: a call must be in the allowset AND the device must
|
|
3167
|
+
// actually have the tool (the registry check inside callDeviceTool) — Core
|
|
3168
|
+
// says what may be asked, the device decides what it will do.
|
|
3169
|
+
//
|
|
3170
|
+
// Origin-checked (only the voice app may set it) and pinned ONCE per session
|
|
3171
|
+
// (a later message cannot widen it). An EMPTY allowset (arrived) refuses
|
|
3172
|
+
// everything but is distinct from NEVER-arrived (frozenTools stays null);
|
|
3173
|
+
// both fail closed here — we never fall back to the phone's own registry,
|
|
3174
|
+
// which is the seam this closes.
|
|
3175
|
+
case "deviceToolAllowset": {
|
|
3176
|
+
const sender = originOf(event?.nativeEvent?.url);
|
|
3177
|
+
const expected = originOf((0, import_core22.getVoiceApiUrl)());
|
|
3178
|
+
if (!sender || !expected || sender !== expected) break;
|
|
3179
|
+
if (Array.isArray(data.tools) && typeof data.sessionId === "string" && data.sessionId !== this.frozenSessionId) {
|
|
3180
|
+
this.frozenTools = new Set(data.tools.filter((n) => typeof n === "string"));
|
|
3181
|
+
this.frozenSessionId = data.sessionId;
|
|
3182
|
+
}
|
|
3183
|
+
break;
|
|
3184
|
+
}
|
|
3066
3185
|
case "device-tool-call": {
|
|
3067
3186
|
const sender = originOf(event?.nativeEvent?.url);
|
|
3068
3187
|
const expected = originOf((0, import_core22.getVoiceApiUrl)());
|
|
@@ -3070,40 +3189,100 @@ var VoiceSessionManager = class {
|
|
|
3070
3189
|
console.warn(`[voiceSession] REFUSED device tool call from "${sender || "unknown origin"}" (expected "${expected}")`);
|
|
3071
3190
|
break;
|
|
3072
3191
|
}
|
|
3073
|
-
void this.
|
|
3192
|
+
void this.handleDeviceToolCall(data);
|
|
3074
3193
|
break;
|
|
3075
3194
|
}
|
|
3076
3195
|
}
|
|
3077
3196
|
}
|
|
3078
|
-
/**
|
|
3197
|
+
/**
|
|
3198
|
+
* One device-tool call, arrival to outcome: validate → authorize → run. The
|
|
3199
|
+
* answer goes back to the mini-app (the model reads it) AND becomes
|
|
3200
|
+
* `lastDeviceTool` — the same object, recorded only once it is known, so a
|
|
3201
|
+
* refused call never looks like one that ran.
|
|
3202
|
+
*/
|
|
3203
|
+
async handleDeviceToolCall(data) {
|
|
3204
|
+
const { correlationId, name, args, timeoutMs } = data || {};
|
|
3205
|
+
const at = Date.now();
|
|
3206
|
+
const epoch = this.sessionEpoch;
|
|
3207
|
+
const toolName = typeof name === "string" ? name : "";
|
|
3208
|
+
const sessionOwner = this.owner;
|
|
3209
|
+
const owner = typeof sessionOwner === "string" && toolName && (0, import_plugin_runtime13.hasDeviceTool)(toolName, sessionOwner) ? sessionOwner : null;
|
|
3210
|
+
let answer;
|
|
3211
|
+
if (!toolName) {
|
|
3212
|
+
answer = { ok: false, reason: "invalid", error: "The device tool call named no tool." };
|
|
3213
|
+
} else if (!correlationId) {
|
|
3214
|
+
answer = {
|
|
3215
|
+
ok: false,
|
|
3216
|
+
reason: "invalid",
|
|
3217
|
+
error: `The call to "${toolName}" carried no correlationId, so it could not be answered.`
|
|
3218
|
+
};
|
|
3219
|
+
} else if (!this.isToolAuthorized(toolName)) {
|
|
3220
|
+
const notReady = this.frozenTools === null;
|
|
3221
|
+
console.warn(`[voiceSession] REFUSED "${toolName}" \u2014 ${notReady ? "allowset not received yet" : "not authorized for this session"}`);
|
|
3222
|
+
answer = notReady ? {
|
|
3223
|
+
ok: false,
|
|
3224
|
+
reason: "not_ready",
|
|
3225
|
+
error: "The voice session is still starting up \u2014 its tool allowset has not arrived yet. Try again in a moment."
|
|
3226
|
+
} : { ok: false, reason: "not_authorized", error: `"${toolName}" is not available in this voice session.` };
|
|
3227
|
+
} else {
|
|
3228
|
+
answer = await this.runDeviceTool(toolName, args, timeoutMs);
|
|
3229
|
+
}
|
|
3230
|
+
if (correlationId) this.postDeviceToolResult({ correlationId, ...answer });
|
|
3231
|
+
if (epoch !== this.sessionEpoch) return;
|
|
3232
|
+
const ms = Math.max(0, Date.now() - at);
|
|
3233
|
+
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) };
|
|
3234
|
+
this.setState({
|
|
3235
|
+
deviceToolCalls: [...this.state.deviceToolCalls.filter((r) => r.owner !== owner), { owner, call }]
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Final authorization = Core's frozen allowset ∩ the current valid local
|
|
3240
|
+
* registry, bound to the ACTIVE session. All must hold:
|
|
3241
|
+
* - a Core allowset arrived and is bound to THIS session (frozenSessionId ===
|
|
3242
|
+
* the active state.sessionId) — a missing or stale/wrong-session allowset
|
|
3243
|
+
* fails closed;
|
|
3244
|
+
* - the tool is IN that allowset (Core approved it); AND
|
|
3245
|
+
* - the tool is STILL registered locally right now (removed-after-approval is
|
|
3246
|
+
* refused; a lazily-added tool Core never approved is refused) — and, on a
|
|
3247
|
+
* clean call, registered by the call's owner.
|
|
3248
|
+
* No fallback to the live registry alone, ever.
|
|
3249
|
+
*/
|
|
3250
|
+
isToolAuthorized(name) {
|
|
3251
|
+
if (typeof name !== "string") return false;
|
|
3252
|
+
if (this.frozenTools === null || this.frozenSessionId === null) return false;
|
|
3253
|
+
if (this.frozenSessionId !== this.state.sessionId) return false;
|
|
3254
|
+
if (!this.frozenTools.has(name)) return false;
|
|
3255
|
+
return (0, import_plugin_runtime13.hasDeviceTool)(name, this.toolScope ?? void 0);
|
|
3256
|
+
}
|
|
3257
|
+
/** Answer the mini-app's pending device-tool request (a call's outcome, or the
|
|
3258
|
+
* tool list) — it resolves the request with this payload by correlationId. */
|
|
3259
|
+
postDeviceToolResult(payload) {
|
|
3260
|
+
this.webView?.injectJavaScript?.(
|
|
3261
|
+
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3262
|
+
JSON.stringify(payload)
|
|
3263
|
+
)}); true;`
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
/** PROPOSE the phone's available tools — this is an offer to Core, not the
|
|
3267
|
+
* authorization set. Core sanitizes it and returns the accepted set in a
|
|
3268
|
+
* `deviceToolAllowset` message, which is what actually gates execution. */
|
|
3079
3269
|
sendDeviceToolList(correlationId) {
|
|
3080
|
-
const
|
|
3270
|
+
const scope = this.toolScope;
|
|
3271
|
+
const tools = (0, import_plugin_runtime13.listDeviceTools)().filter((t) => scope === null || t.pluginId === scope).map((t) => ({
|
|
3081
3272
|
name: t.name,
|
|
3082
3273
|
description: t.description,
|
|
3083
3274
|
input_schema: t.input_schema
|
|
3084
3275
|
}));
|
|
3085
|
-
this.
|
|
3086
|
-
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3087
|
-
JSON.stringify({ correlationId, ok: true, tools })
|
|
3088
|
-
)}); true;`
|
|
3089
|
-
);
|
|
3276
|
+
this.postDeviceToolResult({ correlationId, ok: true, tools });
|
|
3090
3277
|
}
|
|
3091
|
-
/** Run
|
|
3092
|
-
async runDeviceTool(
|
|
3093
|
-
const { correlationId, name, args, timeoutMs } = data || {};
|
|
3094
|
-
if (!correlationId || typeof name !== "string") return;
|
|
3095
|
-
let payload;
|
|
3278
|
+
/** Run an authorized device tool. Never throws: every failure is an answer. */
|
|
3279
|
+
async runDeviceTool(name, args, timeoutMs) {
|
|
3096
3280
|
try {
|
|
3097
3281
|
const outcome = await (0, import_plugin_runtime13.callDeviceTool)(name, args || {}, { timeoutMs });
|
|
3098
|
-
|
|
3282
|
+
return outcome.ok ? { ok: true, result: outcome.result } : { ok: false, reason: outcome.reason, error: outcome.error };
|
|
3099
3283
|
} catch (err) {
|
|
3100
|
-
|
|
3284
|
+
return { ok: false, reason: "failed", error: err?.message || String(err) };
|
|
3101
3285
|
}
|
|
3102
|
-
this.webView?.injectJavaScript?.(
|
|
3103
|
-
`window.__adasDeviceToolResult && window.__adasDeviceToolResult(${JSON.stringify(
|
|
3104
|
-
JSON.stringify(payload)
|
|
3105
|
-
)}); true;`
|
|
3106
|
-
);
|
|
3107
3286
|
}
|
|
3108
3287
|
// ── internals ────────────────────────────────────────────────
|
|
3109
3288
|
setState(partial) {
|
|
@@ -3123,22 +3302,21 @@ var VoiceSessionManager = class {
|
|
|
3123
3302
|
}
|
|
3124
3303
|
}
|
|
3125
3304
|
}
|
|
3126
|
-
waitForConnect() {
|
|
3305
|
+
waitForConnect(caller) {
|
|
3127
3306
|
return new Promise((resolve, reject) => {
|
|
3128
3307
|
const timer = setTimeout(() => {
|
|
3129
3308
|
this.pending = this.pending.filter((p) => p.timer !== timer);
|
|
3130
3309
|
reject(new Error("voice: timed out waiting to connect (is VoiceSessionHost mounted?)"));
|
|
3131
3310
|
}, CONNECT_TIMEOUT_MS);
|
|
3132
|
-
this.pending.push({ resolve, reject, timer });
|
|
3311
|
+
this.pending.push({ caller, resolve, reject, timer });
|
|
3133
3312
|
});
|
|
3134
3313
|
}
|
|
3135
3314
|
resolvePending() {
|
|
3136
|
-
const snap = this.realSnapshot();
|
|
3137
3315
|
const waiting = this.pending;
|
|
3138
3316
|
this.pending = [];
|
|
3139
3317
|
for (const p of waiting) {
|
|
3140
3318
|
clearTimeout(p.timer);
|
|
3141
|
-
p.resolve(
|
|
3319
|
+
p.resolve(this.realSnapshot(p.caller));
|
|
3142
3320
|
}
|
|
3143
3321
|
}
|
|
3144
3322
|
rejectPending(err) {
|
|
@@ -3155,6 +3333,11 @@ var VoiceSessionManager = class {
|
|
|
3155
3333
|
this.rejectPending(new Error("voice: reset"));
|
|
3156
3334
|
this.state = { ...IDLE };
|
|
3157
3335
|
this.owner = null;
|
|
3336
|
+
this.toolScope = null;
|
|
3337
|
+
this.pendingStarts.clear();
|
|
3338
|
+
this.frozenTools = null;
|
|
3339
|
+
this.frozenSessionId = null;
|
|
3340
|
+
this.sessionEpoch++;
|
|
3158
3341
|
this.listeners.clear();
|
|
3159
3342
|
this.webView = null;
|
|
3160
3343
|
}
|