@ateam-ai-mobile/ui 0.1.0 → 0.1.1
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 +79 -22
- package/dist/index.d.ts +79 -22
- package/dist/index.js +2075 -1420
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2072 -1416
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/AdasChatScreen.tsx +4 -0
- package/src/TenantPicker.tsx +131 -0
- package/src/chat/UIActionsRow.tsx +11 -6
- package/src/index.ts +14 -0
- package/src/plugin/EmbeddedPluginWebView.tsx +16 -10
- package/src/plugin/Plugin.tsx +8 -3
- package/src/plugin/WidgetHost.tsx +81 -0
- package/src/providers.ts +17 -0
- package/src/voice/VoiceCallSheet.tsx +330 -0
- package/src/voice/useVoiceCall.ts +200 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ateam-ai-mobile/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Prebuilt ADAS chat experience for React Native \u2014 full-screen chat, chips, widget views. Built on @ateam-ai-mobile/core + @ateam-ai-mobile/plugin-runtime.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"build": "tsup"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@ateam-ai-mobile/core": "0.1.
|
|
13
|
-
"@ateam-ai-mobile/plugin-runtime": "0.1.
|
|
12
|
+
"@ateam-ai-mobile/core": "0.1.1",
|
|
13
|
+
"@ateam-ai-mobile/plugin-runtime": "0.1.1",
|
|
14
14
|
"zustand": "^5.0.0"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
package/src/AdasChatScreen.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import MessageBubble, { DaySeparator } from './chat/MessageBubble';
|
|
|
28
28
|
import TypingIndicator from './chat/TypingIndicator';
|
|
29
29
|
import ChatInput from './chat/ChatInput';
|
|
30
30
|
import Lightbox from './Lightbox';
|
|
31
|
+
import WidgetHost from './plugin/WidgetHost';
|
|
31
32
|
|
|
32
33
|
export interface AdasChatScreenProps {
|
|
33
34
|
/** Called when the user taps the call button; omit to hide it. */
|
|
@@ -152,6 +153,9 @@ export default function AdasChatScreen({
|
|
|
152
153
|
placeholder={placeholder}
|
|
153
154
|
/>
|
|
154
155
|
{withLightbox && <Lightbox />}
|
|
156
|
+
{/* Built-in widget presenter: chips without a host navigator and
|
|
157
|
+
agent-driven focus-plugin events open widgets here — zero wiring. */}
|
|
158
|
+
<WidgetHost />
|
|
155
159
|
</KeyboardAvoidingView>
|
|
156
160
|
);
|
|
157
161
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TenantPicker — optional prebuilt team switcher.
|
|
3
|
+
*
|
|
4
|
+
* Lists the user's REAL tenant memberships via core's listMyTenants() and
|
|
5
|
+
* calls onSelect when one is tapped. The host owns what "switching" means
|
|
6
|
+
* (mint a new tenant-scoped JWT, then re-init) — this component only picks.
|
|
7
|
+
* Single-tenant hosts simply never render it.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
import { View, Text, Pressable, ActivityIndicator, StyleSheet, Platform } from 'react-native';
|
|
11
|
+
import { ada, fontSize, borderRadius, listMyTenants, getTenant } from '@ateam-ai-mobile/core';
|
|
12
|
+
import type { TenantMembership } from '@ateam-ai-mobile/core';
|
|
13
|
+
|
|
14
|
+
const fontFam = Platform.OS === 'ios' ? 'SF Pro Rounded' : undefined;
|
|
15
|
+
|
|
16
|
+
export interface TenantPickerProps {
|
|
17
|
+
/** Called with the picked tenant. Host mints a JWT for it and re-inits. */
|
|
18
|
+
onSelect: (tenant: TenantMembership) => void;
|
|
19
|
+
/** Highlight the active tenant. Defaults to the SDK config's tenant. */
|
|
20
|
+
currentTenantId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function TenantPicker({ onSelect, currentTenantId }: TenantPickerProps) {
|
|
24
|
+
const [tenants, setTenants] = useState<TenantMembership[] | null>(null);
|
|
25
|
+
const [error, setError] = useState<string | null>(null);
|
|
26
|
+
const active = currentTenantId ?? getTenant();
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
listMyTenants()
|
|
31
|
+
.then(list => { if (!cancelled) setTenants(list); })
|
|
32
|
+
.catch(e => { if (!cancelled) setError(e?.message || 'Failed to load teams'); });
|
|
33
|
+
return () => { cancelled = true; };
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
if (error) return <Text style={styles.error}>{error}</Text>;
|
|
37
|
+
if (!tenants) {
|
|
38
|
+
return (
|
|
39
|
+
<View style={styles.loading}>
|
|
40
|
+
<ActivityIndicator color={ada.orange} />
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (tenants.length === 0) return <Text style={styles.empty}>No teams found for this account.</Text>;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View style={styles.list}>
|
|
48
|
+
{tenants.map(t => {
|
|
49
|
+
const isActive = t.id === active;
|
|
50
|
+
return (
|
|
51
|
+
<Pressable
|
|
52
|
+
key={t.id}
|
|
53
|
+
accessibilityRole="button"
|
|
54
|
+
accessibilityLabel={`Team ${t.name}${isActive ? ', current' : ''}`}
|
|
55
|
+
onPress={() => onSelect(t)}
|
|
56
|
+
style={[styles.row, isActive && styles.rowActive]}
|
|
57
|
+
>
|
|
58
|
+
<View style={styles.dot} />
|
|
59
|
+
<View style={styles.rowText}>
|
|
60
|
+
<Text style={styles.name}>{t.name}</Text>
|
|
61
|
+
<Text style={styles.slug}>{t.id}</Text>
|
|
62
|
+
</View>
|
|
63
|
+
{isActive && <Text style={styles.check}>✓</Text>}
|
|
64
|
+
</Pressable>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
list: {
|
|
73
|
+
gap: 10,
|
|
74
|
+
},
|
|
75
|
+
row: {
|
|
76
|
+
flexDirection: 'row',
|
|
77
|
+
alignItems: 'center',
|
|
78
|
+
gap: 12,
|
|
79
|
+
backgroundColor: ada.paperSoft,
|
|
80
|
+
borderRadius: borderRadius.lg,
|
|
81
|
+
borderWidth: 1,
|
|
82
|
+
borderColor: 'transparent',
|
|
83
|
+
paddingHorizontal: 16,
|
|
84
|
+
paddingVertical: 14,
|
|
85
|
+
},
|
|
86
|
+
rowActive: {
|
|
87
|
+
backgroundColor: ada.orangeSoft,
|
|
88
|
+
borderColor: ada.orange,
|
|
89
|
+
},
|
|
90
|
+
dot: {
|
|
91
|
+
width: 10,
|
|
92
|
+
height: 10,
|
|
93
|
+
borderRadius: 5,
|
|
94
|
+
backgroundColor: ada.orange,
|
|
95
|
+
},
|
|
96
|
+
rowText: {
|
|
97
|
+
flex: 1,
|
|
98
|
+
},
|
|
99
|
+
name: {
|
|
100
|
+
color: ada.ink,
|
|
101
|
+
fontSize: fontSize.lg,
|
|
102
|
+
fontWeight: '600',
|
|
103
|
+
fontFamily: fontFam,
|
|
104
|
+
},
|
|
105
|
+
slug: {
|
|
106
|
+
color: ada.inkMuted,
|
|
107
|
+
fontSize: fontSize.sm,
|
|
108
|
+
fontFamily: 'monospace',
|
|
109
|
+
},
|
|
110
|
+
check: {
|
|
111
|
+
color: ada.orangeDeep,
|
|
112
|
+
fontSize: fontSize.lg,
|
|
113
|
+
fontWeight: '700',
|
|
114
|
+
},
|
|
115
|
+
loading: {
|
|
116
|
+
paddingVertical: 24,
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
},
|
|
119
|
+
empty: {
|
|
120
|
+
color: ada.inkMuted,
|
|
121
|
+
fontSize: fontSize.md,
|
|
122
|
+
textAlign: 'center',
|
|
123
|
+
paddingVertical: 16,
|
|
124
|
+
},
|
|
125
|
+
error: {
|
|
126
|
+
color: '#C15545',
|
|
127
|
+
fontSize: fontSize.md,
|
|
128
|
+
textAlign: 'center',
|
|
129
|
+
paddingVertical: 16,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
// Payload comes from `message.actions[]` produced by the polisher and
|
|
3
3
|
// validated by Core's `cp.uiActions_api`.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// Chips targeting a registered surface mount it directly via the surface
|
|
6
|
+
// store. Everything else goes through the navigator registered with
|
|
7
|
+
// setUIActionNavigator() when the host provided one (no router imports
|
|
8
|
+
// here), or falls back to the built-in WidgetHost presenter — so chips open
|
|
9
|
+
// widgets with zero host wiring.
|
|
8
10
|
|
|
9
11
|
import { View, Text, Pressable, StyleSheet, Platform } from 'react-native';
|
|
10
12
|
import { ada, fontSize } from '@ateam-ai-mobile/core';
|
|
11
13
|
import type { UIAction } from '@ateam-ai-mobile/core';
|
|
12
14
|
import { useSurfaceStore, getHaptics } from '@ateam-ai-mobile/plugin-runtime';
|
|
13
15
|
import { getUIActionNavigator } from '../providers';
|
|
16
|
+
import { openWidget } from '../plugin/WidgetHost';
|
|
14
17
|
|
|
15
18
|
const fontFam = Platform.OS === 'ios' ? 'SF Pro Rounded' : undefined;
|
|
16
19
|
|
|
@@ -66,11 +69,13 @@ export default function UIActionsRow({ actions }: Props) {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
const navigate = getUIActionNavigator();
|
|
69
|
-
if (
|
|
70
|
-
|
|
72
|
+
if (navigate) {
|
|
73
|
+
navigate({ pluginId: id, params });
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
|
-
|
|
76
|
+
// No host navigator — present via the built-in WidgetHost (mounted by
|
|
77
|
+
// AdasChatScreen). Params ride along as engine props.
|
|
78
|
+
openWidget(id, Object.keys(params).length ? params : undefined);
|
|
74
79
|
};
|
|
75
80
|
|
|
76
81
|
return (
|
package/src/index.ts
CHANGED
|
@@ -20,19 +20,33 @@ export { default as UIActionsRow } from './chat/UIActionsRow';
|
|
|
20
20
|
// Plugin rendering (native bundles + iframe fallback + surface wrapper)
|
|
21
21
|
export { default as Plugin } from './plugin/Plugin';
|
|
22
22
|
export { default as PluginSurface } from './plugin/PluginSurface';
|
|
23
|
+
// Zero-config widget presenter: chips + agent focus-plugin → open widgets.
|
|
24
|
+
// AdasChatScreen mounts one automatically; custom layouts render <WidgetHost/>
|
|
25
|
+
// once near the root and/or call openWidget(pluginId, props) imperatively.
|
|
26
|
+
export { default as WidgetHost, openWidget, useWidgetHostStore } from './plugin/WidgetHost';
|
|
23
27
|
export { default as EmbeddedPluginWebView } from './plugin/EmbeddedPluginWebView';
|
|
24
28
|
export { usePluginBridge, buildPluginBridgeJS } from './plugin/usePluginBridge';
|
|
25
29
|
|
|
26
30
|
// Widgets
|
|
27
31
|
export { default as WidgetWebView } from './widgets/WidgetWebView';
|
|
28
32
|
|
|
33
|
+
// Team switcher (optional — single-tenant hosts never render it)
|
|
34
|
+
export { default as TenantPicker } from './TenantPicker';
|
|
35
|
+
export type { TenantPickerProps } from './TenantPicker';
|
|
36
|
+
|
|
29
37
|
// Image lightbox
|
|
30
38
|
export { default as Lightbox } from './Lightbox';
|
|
31
39
|
export { useLightboxStore } from './stores/lightboxStore';
|
|
32
40
|
|
|
41
|
+
// Voice (requires `voiceUrl` in the initAdas() config)
|
|
42
|
+
export { default as VoiceCallSheet } from './voice/VoiceCallSheet';
|
|
43
|
+
export { default as useVoiceCall } from './voice/useVoiceCall';
|
|
44
|
+
export type { TranscriptEntry, CallState } from './voice/useVoiceCall';
|
|
45
|
+
|
|
33
46
|
// Host integration
|
|
34
47
|
export {
|
|
35
48
|
setClipboardProvider,
|
|
36
49
|
setUIActionNavigator,
|
|
50
|
+
setPluginWebViewExtraHosts,
|
|
37
51
|
} from './providers';
|
|
38
52
|
export type { ClipboardProvider, UIActionRoute } from './providers';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useState, useMemo, useCallback } from 'react';
|
|
2
2
|
import { View, Text, ActivityIndicator, StyleSheet, Platform } from 'react-native';
|
|
3
3
|
import { WebView } from 'react-native-webview';
|
|
4
|
-
import { getConfig, getCoreApiUrl, getTenant, getToken, ada } from '@ateam-ai-mobile/core';
|
|
4
|
+
import { getConfig, getCoreApiUrl, getVoiceApiUrl, getTenant, getToken, ada } from '@ateam-ai-mobile/core';
|
|
5
5
|
import { resolvePluginUrl, type PlatformPluginManifest as PluginManifest } from '@ateam-ai-mobile/plugin-runtime';
|
|
6
6
|
import { usePluginBridge, buildPluginBridgeJS } from './usePluginBridge';
|
|
7
|
+
import { getPluginWebViewExtraHosts } from '../providers';
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
pluginId: string;
|
|
@@ -29,13 +30,18 @@ export default function EmbeddedPluginWebView({ pluginId, manifest, commandBuffe
|
|
|
29
30
|
const url = resolvePluginUrl(manifest, tenant || undefined);
|
|
30
31
|
const bridgeJS = buildPluginBridgeJS(token || '', tenant || '');
|
|
31
32
|
|
|
32
|
-
// Hosts the plugin may navigate to — derived from the SDK config
|
|
33
|
-
//
|
|
34
|
-
|
|
33
|
+
// Hosts the plugin may navigate to — derived from the SDK config plus any
|
|
34
|
+
// host-registered extras (e.g. a relay service). Resolved LIVE on every
|
|
35
|
+
// read, never frozen at mount: config URLs can change after hydration and a
|
|
36
|
+
// stale whitelist silently bounces plugin URLs out to the system browser
|
|
37
|
+
// (a documented regression in the reference app).
|
|
38
|
+
const computeAllowedHosts = useCallback((): Set<string> => {
|
|
35
39
|
const hosts = new Set<string>();
|
|
36
40
|
const urls = [getCoreApiUrl()];
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
// voiceUrl is optional and may be a getter — resolve via getVoiceApiUrl()
|
|
42
|
+
// (which throws when unset, so gate on the raw config first).
|
|
43
|
+
if (getConfig().voiceUrl) urls.push(getVoiceApiUrl());
|
|
44
|
+
try { urls.push(...(getPluginWebViewExtraHosts()?.() || [])); } catch { /* host getter threw */ }
|
|
39
45
|
for (const u of urls) {
|
|
40
46
|
try { hosts.add(new URL(u).hostname); } catch { /* ignore bad URL */ }
|
|
41
47
|
}
|
|
@@ -49,12 +55,12 @@ export default function EmbeddedPluginWebView({ pluginId, manifest, commandBuffe
|
|
|
49
55
|
try { parsed = new URL(rawUrl); } catch { return false; }
|
|
50
56
|
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return false;
|
|
51
57
|
if (parsed.protocol === 'http:') return false; // belt-and-suspenders with mixedContentMode=never
|
|
52
|
-
return
|
|
53
|
-
}, [
|
|
58
|
+
return computeAllowedHosts().has(parsed.hostname);
|
|
59
|
+
}, [computeAllowedHosts]);
|
|
54
60
|
|
|
55
61
|
const originWhitelist = useMemo(
|
|
56
|
-
() => Array.from(
|
|
57
|
-
[
|
|
62
|
+
() => Array.from(computeAllowedHosts()).map(h => `https://${h}`),
|
|
63
|
+
[computeAllowedHosts],
|
|
58
64
|
);
|
|
59
65
|
|
|
60
66
|
if (error) {
|
package/src/plugin/Plugin.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
useNativeBridge,
|
|
16
16
|
usePluginTheme,
|
|
17
17
|
getPluginNative,
|
|
18
|
+
gatePluginNative,
|
|
18
19
|
type PluginContext,
|
|
19
20
|
} from '@ateam-ai-mobile/plugin-runtime';
|
|
20
21
|
import EmbeddedPluginWebView from './EmbeddedPluginWebView';
|
|
@@ -84,7 +85,11 @@ export default function Plugin({ id, manifest, commandBufferRef, props: enginePr
|
|
|
84
85
|
|
|
85
86
|
// Wire up bridge, capabilities, theme
|
|
86
87
|
const bridge = useNativeBridge(id, connectorId, commandBufferRef);
|
|
87
|
-
|
|
88
|
+
// Per-plugin capability gate: undeclared native APIs become warn-and-ignore
|
|
89
|
+
// proxies. Memoized so the plugin Component doesn't re-render on a fresh
|
|
90
|
+
// object every pass (useApi effects key off prop identity).
|
|
91
|
+
const declaredCaps = manifest?.native;
|
|
92
|
+
const native = useMemo(() => gatePluginNative(getPluginNative(), declaredCaps), [declaredCaps]);
|
|
88
93
|
const theme = usePluginTheme();
|
|
89
94
|
|
|
90
95
|
// Build context from the host-owned identity
|
|
@@ -93,8 +98,8 @@ export default function Plugin({ id, manifest, commandBufferRef, props: enginePr
|
|
|
93
98
|
tenant: getTenant(),
|
|
94
99
|
pluginId: id,
|
|
95
100
|
connectorId,
|
|
96
|
-
user: user ? { id: user.actorId || '', name: user.name || '', roles: [] } : undefined,
|
|
97
|
-
}), [id, connectorId, user?.actorId, user?.name]);
|
|
101
|
+
user: user ? { id: user.actorId || '', name: user.name || '', roles: user.roles || [] } : undefined,
|
|
102
|
+
}), [id, connectorId, user?.actorId, user?.name, user?.roles]);
|
|
98
103
|
|
|
99
104
|
// Fallback: connector declared an RN component but didn't ship the bundle.
|
|
100
105
|
// When the manifest also has an iframeUrl (adaptive mode), render the
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WidgetHost — zero-config widget presenter.
|
|
3
|
+
*
|
|
4
|
+
* The glue that makes chips and the agent able to open widgets without any
|
|
5
|
+
* host wiring:
|
|
6
|
+
*
|
|
7
|
+
* - `openWidget(pluginId, opts)` — imperative open from anywhere. Surfaces
|
|
8
|
+
* registered in the surface store (host surfaces, platform-declared
|
|
9
|
+
* surfaces) mount through the surface registry; everything else is
|
|
10
|
+
* presented here as a fullscreen PluginSurface (manifest fetched lazily).
|
|
11
|
+
* - Chip taps: UIActionsRow falls back to openWidget() when the host hasn't
|
|
12
|
+
* registered a custom navigator via setUIActionNavigator().
|
|
13
|
+
* - Server/agent proactive open: listens for the chat stream's
|
|
14
|
+
* `focus-plugin` event (sys.focusUiPlugin(pluginId, props) on the engine
|
|
15
|
+
* side) and presents the widget, forwarding the engine props.
|
|
16
|
+
*
|
|
17
|
+
* <AdasChatScreen /> mounts a WidgetHost automatically — drop-in hosts get
|
|
18
|
+
* the full chips→widgets and agent→widgets experience for free. Custom
|
|
19
|
+
* layouts render <WidgetHost /> once near the app root (mounting it twice is
|
|
20
|
+
* safe: both instances read the same store, last-closed wins).
|
|
21
|
+
*/
|
|
22
|
+
import { useEffect } from 'react';
|
|
23
|
+
import { create } from 'zustand';
|
|
24
|
+
import { pluginCommandEmitter } from '@ateam-ai-mobile/core';
|
|
25
|
+
import { useSurfaceStore } from '@ateam-ai-mobile/plugin-runtime';
|
|
26
|
+
import PluginSurface from './PluginSurface';
|
|
27
|
+
|
|
28
|
+
interface WidgetHostState {
|
|
29
|
+
pluginId: string | null;
|
|
30
|
+
/** Engine/chip-supplied props forwarded to the plugin as PluginProps.props. */
|
|
31
|
+
props?: any;
|
|
32
|
+
open: (pluginId: string, props?: any) => void;
|
|
33
|
+
close: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useWidgetHostStore = create<WidgetHostState>((set) => ({
|
|
37
|
+
pluginId: null,
|
|
38
|
+
props: undefined,
|
|
39
|
+
open: (pluginId, props) => set({ pluginId, props }),
|
|
40
|
+
close: () => set({ pluginId: null, props: undefined }),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Open a widget by plugin id. Host surfaces registered in the surface store
|
|
45
|
+
* take priority (they own their chrome); everything else is presented as a
|
|
46
|
+
* fullscreen PluginSurface by the mounted <WidgetHost />.
|
|
47
|
+
*/
|
|
48
|
+
export function openWidget(pluginId: string, props?: any): void {
|
|
49
|
+
const surfaces = useSurfaceStore.getState();
|
|
50
|
+
if (surfaces.manifests[pluginId]) {
|
|
51
|
+
surfaces.show(pluginId, props);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
useWidgetHostStore.getState().open(pluginId, props);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function WidgetHost() {
|
|
58
|
+
const pluginId = useWidgetHostStore(s => s.pluginId);
|
|
59
|
+
const props = useWidgetHostStore(s => s.props);
|
|
60
|
+
const close = useWidgetHostStore(s => s.close);
|
|
61
|
+
|
|
62
|
+
// Agent-driven proactive open: sys.focusUiPlugin(pluginId, props) flows
|
|
63
|
+
// through the chat stream → core emits 'focus-plugin' → present it.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
return pluginCommandEmitter.on('focus-plugin', (data: any) => {
|
|
66
|
+
if (data?.pluginId) openWidget(String(data.pluginId), data.props);
|
|
67
|
+
});
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
if (!pluginId) return null;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<PluginSurface
|
|
74
|
+
key={pluginId}
|
|
75
|
+
pluginId={pluginId}
|
|
76
|
+
surface={{ type: 'fullscreen' }}
|
|
77
|
+
dismiss={close}
|
|
78
|
+
props={props}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
package/src/providers.ts
CHANGED
|
@@ -44,3 +44,20 @@ export function setUIActionNavigator(navigate: UIActionNavigator | null): void {
|
|
|
44
44
|
export function getUIActionNavigator(): UIActionNavigator | null {
|
|
45
45
|
return _navigator;
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
// ── Plugin WebView extra allowed hosts ─────────────────────────────────────
|
|
49
|
+
// The plugin WebView allow-list is derived from the SDK config (core + voice
|
|
50
|
+
// URLs). Hosts with additional trusted services plugins navigate to (e.g. a
|
|
51
|
+
// relay) register a getter returning extra URLs; resolved live per check.
|
|
52
|
+
|
|
53
|
+
type ExtraHostsGetter = () => string[];
|
|
54
|
+
|
|
55
|
+
let _extraHosts: ExtraHostsGetter | null = null;
|
|
56
|
+
|
|
57
|
+
export function setPluginWebViewExtraHosts(getter: ExtraHostsGetter | null): void {
|
|
58
|
+
_extraHosts = getter;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getPluginWebViewExtraHosts(): ExtraHostsGetter | null {
|
|
62
|
+
return _extraHosts;
|
|
63
|
+
}
|