@unoverse-platform/studio 0.1.103 → 0.1.105
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/local/sample-app/chat/manifest.yaml +2 -2
- package/local/sample-component/text-chat/components/suggestions.yaml +1 -1
- package/local/sample-component/text-chat/layouts/chat-welcome.yaml +1 -1
- package/local/sample-component/text-chat/text-chat.yaml +3 -0
- package/local/scaffold.d.mts +5 -2
- package/local/scaffold.mjs +68 -1
- package/package.json +4 -2
- package/screens/AtomsView.tsx +2 -1
- package/screens/ComponentsView.tsx +2 -1
- package/screens/NativeAppHost.tsx +1 -2
- package/screens/TemplatesLibraryView.tsx +14 -1
- package/screens/TemplatesView.tsx +2 -1
- package/screens/index.ts +4 -0
- package/screens/react/unoverse.tsx +281 -0
- package/vendor/base/src/agent-mcp/index.ts +1 -1
- package/vendor/base/src/agent-mcp/invoke.ts +6 -2
- package/vendor/base/src/agent-mcp/tools.ts +14 -5
- package/vendor/base/src/agent-mcp/types.ts +6 -0
- package/vendor/base/src/auth/auth.ts +90 -23
- package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
- package/vendor/base/src/definitions/load/compile/template.ts +23 -2
- package/vendor/base/src/definitions/model.ts +9 -0
- package/vendor/base/src/definitions/registry/catalog.ts +6 -7
- package/vendor/base/src/executor.ts +25 -1
- package/vendor/base/src/lint/design/index.mjs +9 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
- package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
- package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
- package/vendor/base/src/platform/serviceCalls.ts +13 -0
- package/vendor/sdk/appEngine.ts +363 -0
- package/vendor/sdk/lib/actions.ts +36 -0
- package/vendor/sdk/lib/citations.ts +48 -0
- package/vendor/sdk/lib/core/apps.ts +18 -0
- package/vendor/sdk/lib/core/client.ts +3 -3
- package/vendor/sdk/lib/core/store.ts +6 -0
- package/vendor/sdk/lib/dom/app.ts +546 -0
- package/vendor/sdk/lib/dom/chrome.ts +125 -0
- package/vendor/sdk/lib/dom/component.ts +333 -0
- package/vendor/sdk/lib/dom/connection.ts +309 -0
- package/vendor/sdk/lib/dom/element.ts +69 -0
- package/vendor/sdk/lib/dom/isolate.ts +37 -0
- package/vendor/sdk/lib/dom/markdown.ts +388 -0
- package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
- package/vendor/sdk/lib/dom/mounted.ts +29 -0
- package/vendor/sdk/lib/dom/primitives.ts +689 -0
- package/vendor/sdk/lib/dom/region.ts +195 -0
- package/vendor/sdk/lib/dom/render.ts +177 -0
- package/vendor/sdk/lib/dom/theme.ts +38 -0
- package/vendor/sdk/lib/dom/types.ts +36 -0
- package/vendor/sdk/lib/index.ts +26 -17
- package/vendor/sdk/lib/orb/canvas.ts +139 -0
- package/vendor/sdk/lib/orb/index.ts +8 -18
- package/vendor/sdk/lib/orb/presets.ts +1 -1
- package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
- package/vendor/sdk/lib/realtime/capture.ts +152 -0
- package/vendor/sdk/lib/realtime/playback.ts +129 -0
- package/vendor/sdk/lib/realtime/socket.ts +117 -0
- package/vendor/sdk/lib/style.ts +45 -37
- package/vendor/sdk/lib/voiceService.ts +344 -0
- package/vendor/sdk/main.ts +107 -0
- package/vendor/sdk/mcpAppBridge.ts +125 -102
- package/vendor/sdk/pageInterfaces.ts +452 -0
- package/vendor/sdk/responseView.ts +193 -0
- package/vite.config.ts +3 -3
- package/vendor/sdk/appEngine.tsx +0 -395
- package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
- package/vendor/sdk/lib/app.tsx +0 -730
- package/vendor/sdk/lib/connection.tsx +0 -433
- package/vendor/sdk/lib/isolate.tsx +0 -70
- package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
- package/vendor/sdk/lib/orb/theme.ts +0 -83
- package/vendor/sdk/lib/primitives.tsx +0 -943
- package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
- package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
- package/vendor/sdk/lib/render.tsx +0 -244
- package/vendor/sdk/lib/streamed.tsx +0 -30
- package/vendor/sdk/lib/theme.ts +0 -55
- package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
- package/vendor/sdk/lib/transition/index.ts +0 -9
- package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
- package/vendor/sdk/lib/voice.tsx +0 -422
- package/vendor/sdk/main.tsx +0 -125
- package/vendor/sdk/pageInterfaces.tsx +0 -388
- package/vendor/sdk/responseView.tsx +0 -82
package/vendor/sdk/appEngine.tsx
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The native app ENGINE (§6d) — surface-agnostic. It:
|
|
3
|
-
* - reads its OWN workflow binding from its manifest (§4b: "App supplies workflow, trigger"),
|
|
4
|
-
* - opens the SDK's native connection (`/stream` register_session → store),
|
|
5
|
-
* - fires the workflow DIRECTLY via `conn.sendMessage` → REST `{apiUrl}/api/workflows/:id/execute`
|
|
6
|
-
* with the USER's token (§5a: "direct channel → Gravity backend — never brokered").
|
|
7
|
-
*
|
|
8
|
-
* The server tool only LOADS this app; it never fires or brokers a token. Uses the LOCAL
|
|
9
|
-
* vendored SDK (./lib), not the npm package.
|
|
10
|
-
*/
|
|
11
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
-
import { postToEmbedHost } from "./mcpAppBridge";
|
|
13
|
-
import { PageInterfaces, type Binding } from "./pageInterfaces";
|
|
14
|
-
import {
|
|
15
|
-
UnoverseClient,
|
|
16
|
-
ComponentStore,
|
|
17
|
-
StreamedUnoverseApp,
|
|
18
|
-
INERT_VOICE,
|
|
19
|
-
useVoiceService,
|
|
20
|
-
useUnoverseConnection,
|
|
21
|
-
useUnoverseTheme,
|
|
22
|
-
computeAppWidth,
|
|
23
|
-
resolveActiveLayout,
|
|
24
|
-
setAnalyticsSink,
|
|
25
|
-
setAnalyticsContext,
|
|
26
|
-
setAnalyticsDebug,
|
|
27
|
-
type UseVoiceServiceConfig,
|
|
28
|
-
type UnoverseDefinition,
|
|
29
|
-
type ActionMeta,
|
|
30
|
-
} from "./lib";
|
|
31
|
-
|
|
32
|
-
export interface AppConfig {
|
|
33
|
-
serverUrl: string;
|
|
34
|
-
apiUrl?: string;
|
|
35
|
-
appId: string;
|
|
36
|
-
getAccessToken?: () => Promise<string | null> | string | null;
|
|
37
|
-
userId?: string;
|
|
38
|
-
conversationId: string;
|
|
39
|
-
/** Where this app is running, reported on every analytics event
|
|
40
|
-
* (UNOVERSE_ANALYTICS.md §surfaces). Host-supplied: the app cannot know which frame
|
|
41
|
-
* it was placed in. */
|
|
42
|
-
surface?: string;
|
|
43
|
-
/** Log each analytics event as it fires. Server-delivered with the rest of the tenant
|
|
44
|
-
* config, never a client-side flag someone can turn on against a live site. */
|
|
45
|
-
analyticsDebug?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function AppEngine(cfg: AppConfig) {
|
|
49
|
-
const { serverUrl, apiUrl, appId: configuredApp, getAccessToken, userId, conversationId, surface, analyticsDebug } = cfg;
|
|
50
|
-
|
|
51
|
-
// ONE STORE PER CONVERSATION. The host may reuse this engine (the native-host
|
|
52
|
-
// iframe posts new config on app switch), and a template swap keeps the store by
|
|
53
|
-
// law (the template owns nothing) — but a CONVERSATION swap must not: carrying the
|
|
54
|
-
// old timeline across conversations rendered one app's history inside another's
|
|
55
|
-
// shell (observed live, 2026-08-08: one org's chat inside another org's app).
|
|
56
|
-
const store = useMemo(() => new ComponentStore(), [userId, conversationId]);
|
|
57
|
-
const client = useMemo(
|
|
58
|
-
() => new UnoverseClient(`${serverUrl}/mcp`, { getAccessToken: getAccessToken as any }),
|
|
59
|
-
[serverUrl],
|
|
60
|
-
);
|
|
61
|
-
const [binding, setBinding] = useState<{ workflow?: string; trigger?: string; service?: string }>({});
|
|
62
|
-
// RENDER-TIME reset (React's derived-state-from-props pattern), not an effect: on an
|
|
63
|
-
// app switch the connection effect and a reset effect share one commit, and the
|
|
64
|
-
// connection runs first — so an effect-based reset still let one session open with the
|
|
65
|
-
// OLD app's trigger against the NEW app's conversation before correcting (observed
|
|
66
|
-
// live 2026-08-16, twice: first with no reset, then with the reset as an effect).
|
|
67
|
-
// Setting state during render makes React re-render BEFORE the commit, so the
|
|
68
|
-
// connection effect only ever sees either the old app intact or an empty binding —
|
|
69
|
-
// never the cross-app hybrid.
|
|
70
|
-
// WHICH APP IS RUNNING, and there are two ways it changes. The host re-posts its
|
|
71
|
-
// config (the native-host iframe's app switch), and the SERVER can now swap it mid
|
|
72
|
-
// conversation — an app discovered and called IS the surface, so opening one replaces
|
|
73
|
-
// what is mounted (owner ruling 2026-09-07). One piece of state either way, so the
|
|
74
|
-
// stale-binding guard below covers both rather than only the host's.
|
|
75
|
-
const [appId, setAppId] = useState(configuredApp);
|
|
76
|
-
const [boundFor, setBoundFor] = useState(configuredApp);
|
|
77
|
-
// The host's own switch WINS and resets: a new config is a new intent.
|
|
78
|
-
if (boundFor !== configuredApp) {
|
|
79
|
-
setBoundFor(configuredApp);
|
|
80
|
-
setAppId(configuredApp);
|
|
81
|
-
setBinding({});
|
|
82
|
-
}
|
|
83
|
-
// The server's switch, same reset: a different app is a different workflow and trigger,
|
|
84
|
-
// and the binding is read async from the incoming app's manifest (the effect below).
|
|
85
|
-
// Clearing it here is what stops a session opening on the OLD app's trigger against the
|
|
86
|
-
// new app — the hazard the render-time reset above exists for.
|
|
87
|
-
const switchApp = (next: string) => {
|
|
88
|
-
if (next === appId) return;
|
|
89
|
-
setAppId(next);
|
|
90
|
-
setBinding({});
|
|
91
|
-
};
|
|
92
|
-
// STATE-OWNED WIDTH (design data): the layout root's `appWidth` = the core surface's
|
|
93
|
-
// constant width; an occupied panel slot's `appWidth` adds to it (computeAppWidth).
|
|
94
|
-
// No declared width → the host keeps its own default. There is no other path.
|
|
95
|
-
// THE WHOLE DEFINITION, not a chosen four fields. This used to keep `{root, org, layouts,
|
|
96
|
-
// defaultLayout}` and drop everything else, which is a whitelist that silently loses each
|
|
97
|
-
// field the contract gains. It was dropping `stateOrder`, `publicStates`, `initialView`
|
|
98
|
-
// and `stateTree`, quietly running the v2 ladder in legacy mode, and it dropped `defs`
|
|
99
|
-
// too: with shared pieces unreachable, the core panel's `appWidth` went uncounted and the
|
|
100
|
-
// app's width collapsed to the open surface's remainder term, which is negative, so the
|
|
101
|
-
// app rendered 0 pixels wide and blank.
|
|
102
|
-
const [def, setDef] = useState<UnoverseDefinition | null>(null);
|
|
103
|
-
const lastWidthRef = useRef<string | undefined>(undefined);
|
|
104
|
-
// Named app sizes resolve via the served theme (theme.appSize), like every token.
|
|
105
|
-
// The theme's name is the template's org. When appId already carries it
|
|
106
|
-
// ("org/name" — every production boot), start the theme read IMMEDIATELY, in
|
|
107
|
-
// parallel with the template read; waiting for the definition just to learn an org
|
|
108
|
-
// we were handed serialized two round trips for nothing. Bare ids (dev) still wait.
|
|
109
|
-
const orgFromId = appId.includes("/") ? appId.split("/")[0] : null;
|
|
110
|
-
const theme = useUnoverseTheme(
|
|
111
|
-
client,
|
|
112
|
-
orgFromId ? `${orgFromId}/light` : def ? (def.org ? `${def.org}/light` : "light") : null,
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
// The app owns which workflow it runs — read it from its manifest (§4b), not from config.
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
let alive = true;
|
|
118
|
-
// The stale-binding reset lives at RENDER time (boundFor, above) — resetting here,
|
|
119
|
-
// in an effect, ran too late: the connection effect had already opened a session on
|
|
120
|
-
// the previous app's trigger within the same commit.
|
|
121
|
-
client
|
|
122
|
-
.connect()
|
|
123
|
-
.then(() => (client as any).client.readResource({ uri: `unoverse://apps/${appId.toLowerCase()}` }))
|
|
124
|
-
.then((res: any) => {
|
|
125
|
-
const parsed = res?.contents?.[0]?.text ? JSON.parse(res.contents[0].text) : null;
|
|
126
|
-
const m = parsed?.manifest ?? parsed ?? {};
|
|
127
|
-
const b = m.binding ?? {};
|
|
128
|
-
// `service` (e.g. "voice") is the app's own declaration — it selects whether the
|
|
129
|
-
// renderer runs a native service. Read it here, next to the binding, and forward it.
|
|
130
|
-
if (alive) setBinding({ workflow: b.workflow, trigger: b.trigger, service: m.service });
|
|
131
|
-
})
|
|
132
|
-
.catch((e) => console.error("[app] manifest read failed:", e));
|
|
133
|
-
// The width lives in the template DATA (root/slot `appWidth`), so read the
|
|
134
|
-
// definition too — same resource the renderer loads.
|
|
135
|
-
client
|
|
136
|
-
.connect()
|
|
137
|
-
.then(() => client.readDefinition(`unoverse://apps/${appId}/definition`))
|
|
138
|
-
.then((d) => {
|
|
139
|
-
if (alive) setDef(d ?? null);
|
|
140
|
-
})
|
|
141
|
-
.catch((e) => console.error("[app] template read failed:", e));
|
|
142
|
-
return () => {
|
|
143
|
-
alive = false;
|
|
144
|
-
};
|
|
145
|
-
}, [client, appId]);
|
|
146
|
-
|
|
147
|
-
// Report the app's width UP to the embedding host (iframe boundary → the twin of the
|
|
148
|
-
// inbound `unoverse:config` channel), re-reporting whenever a panel surface opens or
|
|
149
|
-
// closes: the app grows by the panel's declared width, then shrinks back. The host
|
|
150
|
-
// animates the resize. Only posts on an ACTUAL width change (guard).
|
|
151
|
-
// ANALYTICS (UNOVERSE_ANALYTICS.md). The SDK produces events and knows no destination;
|
|
152
|
-
// this is the pipe out. Events cross to the HOST REALM, where the page's own analytics
|
|
153
|
-
// tag lives with the visitor's cookie and consent state already resolved — which is the
|
|
154
|
-
// whole reason measurement runs client-side rather than from a server that cannot read
|
|
155
|
-
// either. With no parent (a first-party page, an MCP app host) the post is a silent
|
|
156
|
-
// no-op, so a surface with nothing to report to costs nothing.
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
setAnalyticsDebug(!!analyticsDebug);
|
|
159
|
-
setAnalyticsContext({ surface, conversation_id: conversationId, user_id: userId });
|
|
160
|
-
setAnalyticsSink((event) => {
|
|
161
|
-
postToEmbedHost({ type: "unoverse:event", ...event });
|
|
162
|
-
});
|
|
163
|
-
return () => setAnalyticsSink(null);
|
|
164
|
-
}, [surface, conversationId, userId, analyticsDebug]);
|
|
165
|
-
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
// A NEW APP RE-ANNOUNCES ITS WIDTH, ALWAYS. The native host keeps ONE iframe for every
|
|
168
|
-
// app (its srcdoc is keyed on the server, not the app — it just re-posts
|
|
169
|
-
// `unoverse:config` with a new appId) and this engine is re-rendered in place, so
|
|
170
|
-
// the change-guard below outlives the switch — while the host RESETS its frame to the
|
|
171
|
-
// default on it (NativeAppHost/TemplatesView). Two apps of the same declared width is
|
|
172
|
-
// the ordinary case (one org, one `chat` size), and the incoming app then reported
|
|
173
|
-
// nothing: its frame sat at the host default (100%) while its core panel held its
|
|
174
|
-
// declared width, leaving a dead band beside it (observed live 2026-08-13,
|
|
175
|
-
// bpp-voice). Clearing the guard here makes the first report after an app
|
|
176
|
-
// switch unconditional; the guard still suppresses repeats within one app.
|
|
177
|
-
lastWidthRef.current = undefined;
|
|
178
|
-
const report = () => {
|
|
179
|
-
// NAME-SYNC: width is computed off the ACTIVE layout (multi-layout templates
|
|
180
|
-
// swap whole arrangements); single-layout defs resolve to their root.
|
|
181
|
-
const w = computeAppWidth(resolveActiveLayout(def, store), store, theme?.appSize, def);
|
|
182
|
-
if (w && w !== lastWidthRef.current) {
|
|
183
|
-
lastWidthRef.current = w;
|
|
184
|
-
postToEmbedHost({ type: "unoverse:size", width: w });
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
report();
|
|
188
|
-
return store.subscribe(report);
|
|
189
|
-
}, [store, def, theme]);
|
|
190
|
-
|
|
191
|
-
// VOICE (§2e-1): a `service: "voice"` app runs the native voice service — which now lives
|
|
192
|
-
// in the shared renderer (StreamedUnoverseApp), identical to Studio. The host only
|
|
193
|
-
// supplies the connection identity it already holds; startCall/endCall/toggleMute are
|
|
194
|
-
// answered inside the renderer, so this host wires NO voice buttons. Absent → inert.
|
|
195
|
-
const voiceConfig = useMemo<UseVoiceServiceConfig | undefined>(
|
|
196
|
-
() =>
|
|
197
|
-
// THE SERVICE NEEDS NO WORKFLOW OF ITS OWN. It is the page's microphone, speaker and
|
|
198
|
-
// audio lane; a call names the workflow it runs (a page template's binding,
|
|
199
|
-
// UNOVERSE_HOST_PAGE.md rule 4), and an app with a binding lends it as the default.
|
|
200
|
-
binding.service === "voice"
|
|
201
|
-
? {
|
|
202
|
-
// START_CALL REST triggers the workflow on the gateway (user token). The audio
|
|
203
|
-
// WS lane is derived by the SDK from the client's server origin (see the renderer)
|
|
204
|
-
// — the host passes NO wsUrl (its srcdoc `location.origin` is "null").
|
|
205
|
-
apiUrl: apiUrl ?? serverUrl,
|
|
206
|
-
getAccessToken: getAccessToken as UseVoiceServiceConfig["getAccessToken"],
|
|
207
|
-
session: {
|
|
208
|
-
userId: userId ?? "",
|
|
209
|
-
conversationId,
|
|
210
|
-
chatId: conversationId,
|
|
211
|
-
workflowId: binding.workflow ?? "",
|
|
212
|
-
targetTriggerNode: binding.trigger ?? "",
|
|
213
|
-
},
|
|
214
|
-
// Producer: writes callState/isMuted into TEMPLATE STATE so the voice states switch.
|
|
215
|
-
store,
|
|
216
|
-
}
|
|
217
|
-
: undefined,
|
|
218
|
-
[binding.service, binding.workflow, binding.trigger, apiUrl, serverUrl, getAccessToken, userId, conversationId, store],
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// ONE CALL FOR THE PAGE. The service runs here, once, so the app and the page's own
|
|
222
|
-
// templates (an avatar in the navigation) start, follow and end the same call.
|
|
223
|
-
const voice = useVoiceService(
|
|
224
|
-
voiceConfig ? { ...voiceConfig, wsUrl: client.serverOrigin.replace(/^http/, "ws") } : INERT_VOICE,
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
// CONNECTED TO THE TASK UNTIL THE PAGE CLOSES. The avatar is a button that starts a task's
|
|
228
|
-
// workflow; from that press the page is in the task (its steps, its submit) until the task
|
|
229
|
-
// closes or the visitor leaves. Whether the workflow has a voice is its own business, so
|
|
230
|
-
// nothing here waits on a call (UNOVERSE_HOST_PAGE.md rule 4).
|
|
231
|
-
const [inTask, setInTask] = useState(false);
|
|
232
|
-
|
|
233
|
-
// SDK's native connection: opens /stream, register_session, feeds the store. Outbound
|
|
234
|
-
// sendMessage fires the workflow DIRECTLY (REST, user token) — no server broker.
|
|
235
|
-
const conn = useUnoverseConnection(
|
|
236
|
-
{ apiUrl: apiUrl ?? serverUrl, streamUrl: serverUrl, getAccessToken: getAccessToken as any },
|
|
237
|
-
{
|
|
238
|
-
workflowId: binding.workflow ?? "",
|
|
239
|
-
targetTriggerNode: binding.trigger ?? "",
|
|
240
|
-
userId: userId ?? "",
|
|
241
|
-
conversationId,
|
|
242
|
-
chatId: conversationId,
|
|
243
|
-
appId,
|
|
244
|
-
},
|
|
245
|
-
store,
|
|
246
|
-
// Open once the user is known AND there is something to hear from: the app's own workflow,
|
|
247
|
-
// or a task the page is in. An app with no workflow (a website's page app) connects when the
|
|
248
|
-
// avatar starts a task and stays connected to it; nothing is held open while the visitor
|
|
249
|
-
// only reads.
|
|
250
|
-
{
|
|
251
|
-
enabled: !!userId && (!!binding.workflow || voice.state.isCallActive || inTask),
|
|
252
|
-
onAppSwitch: switchApp,
|
|
253
|
-
// A voice in the running workflow asks the page in: join its call, never start one.
|
|
254
|
-
onJoinCall: (workflowId) => void voice.joinCall(workflowId),
|
|
255
|
-
},
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
// STATUS TO THE HOST. In native-host mode the iframe owns the only real
|
|
259
|
-
// connection, so a host badge watching its own (never-opened) in-page connection
|
|
260
|
-
// reads "offline" forever while this session streams (observed live). Report the
|
|
261
|
-
// truth the same way size is reported; hosts that don't listen lose nothing.
|
|
262
|
-
//
|
|
263
|
-
// `failure` is the last run's WORKFLOW_ERROR detail (which node, what error) —
|
|
264
|
-
// DEVELOPER information. A production embed host doesn't listen and its users see
|
|
265
|
-
// nothing; Studio's preview listens and shows it, so a dead run stops being a
|
|
266
|
-
// silence only the server logs could explain.
|
|
267
|
-
const [failure, setFailure] = useState(store.getLastFailure());
|
|
268
|
-
useEffect(() => store.subscribe(() => setFailure(store.getLastFailure())), [store]);
|
|
269
|
-
useEffect(() => {
|
|
270
|
-
// `refused` carries the server's terminal refusal text (register_session isError) —
|
|
271
|
-
// the one state a "Connecting…" badge must not sit on forever.
|
|
272
|
-
postToEmbedHost({
|
|
273
|
-
type: "unoverse:status",
|
|
274
|
-
ready: !!conn.isReady,
|
|
275
|
-
connected: !!conn.isConnected,
|
|
276
|
-
refused: conn.refusal ?? undefined,
|
|
277
|
-
failure: failure ?? undefined,
|
|
278
|
-
});
|
|
279
|
-
}, [conn.isReady, conn.isConnected, conn.refusal, failure]);
|
|
280
|
-
|
|
281
|
-
// THE SPEC'S ANSWERS, AND NOTHING ELSE (UNOVERSE_MCP_COMPLETE_GUIDE.md, "How the SDK answers an
|
|
282
|
-
// elicitation"): while the server is ELICITING on this session, a component's `submit` answers
|
|
283
|
-
// `accept` with its slice and its `cancel` answers `cancel`. Every other action (a state it
|
|
284
|
-
// enters, a step, a tab) is the component's own and goes to the server as usual. Without a
|
|
285
|
-
// pending elicitation, a submit falls back to the data-plane `user_action` waiter (a workflow
|
|
286
|
-
// node waiting on a component). One way for every component, the app's and the page's alike.
|
|
287
|
-
const toServer = (t: string, data: Record<string, unknown>, meta?: ActionMeta) => {
|
|
288
|
-
const state = (meta?.state ?? data) as Record<string, unknown>;
|
|
289
|
-
if ((t === "submit" || t === "cancel") && conn.resolveElicitation(state, t === "cancel" ? "cancel" : "accept")) return;
|
|
290
|
-
conn.sendAction(t, {
|
|
291
|
-
componentNodeId: meta?.nodeId,
|
|
292
|
-
componentType: meta?.type,
|
|
293
|
-
// The component's OWN chatId is the TURN id — the waiting node execution is
|
|
294
|
-
// keyed `<turn>:<nodeId>`, not the conversation-level chatId.
|
|
295
|
-
componentChatId: meta?.chatId,
|
|
296
|
-
componentState: state,
|
|
297
|
-
});
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// A PAGE TEMPLATE'S TASK, called at its own workflow's trigger over REST with the user's
|
|
301
|
-
// token, as any request to a workflow (§5a). `metadata.task` names the task its Task Runner
|
|
302
|
-
// loads. The avatar's press is the first call; the task's submit is the last, carrying the
|
|
303
|
-
// answers as `metadata.submit`, and nothing waits for it (design/tasks.md, "The submit").
|
|
304
|
-
// From the first call the page is in the task.
|
|
305
|
-
const startTask = async (b: Binding, task: string, submit?: Record<string, unknown>) => {
|
|
306
|
-
if (!b.workflow) return;
|
|
307
|
-
setInTask(true);
|
|
308
|
-
const token = getAccessToken ? await getAccessToken() : null;
|
|
309
|
-
fetch(`${apiUrl ?? serverUrl}/api/workflows/${b.workflow}/execute`, {
|
|
310
|
-
method: "POST",
|
|
311
|
-
headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
312
|
-
body: JSON.stringify({
|
|
313
|
-
input: {
|
|
314
|
-
message: submit ? "Submit" : "Start",
|
|
315
|
-
chatId: conversationId,
|
|
316
|
-
conversationId,
|
|
317
|
-
userId,
|
|
318
|
-
providerId: "gravity-ds",
|
|
319
|
-
metadata: { targetTriggerNode: b.trigger, workflowId: b.workflow, task, ...(submit ? { submit } : {}) },
|
|
320
|
-
},
|
|
321
|
-
conversationId,
|
|
322
|
-
}),
|
|
323
|
-
}).catch((e) => console.error("[unoverse] task start failed", e));
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
return (
|
|
327
|
-
<>
|
|
328
|
-
{/* The page's own templates, drawn into its elements by this one runtime (pageInterfaces.tsx). */}
|
|
329
|
-
<PageInterfaces org={appId.split("/")[0]} client={client} voice={voice} appStore={store} toServer={toServer} startTask={startTask} />
|
|
330
|
-
<StreamedUnoverseApp
|
|
331
|
-
client={client}
|
|
332
|
-
store={store}
|
|
333
|
-
uri={`unoverse://apps/${appId}/definition`}
|
|
334
|
-
voice={voiceConfig}
|
|
335
|
-
voiceService={voice}
|
|
336
|
-
onAction={(action, data, meta) => {
|
|
337
|
-
const t = typeof action === "string" ? action : (action as any)?.type;
|
|
338
|
-
if (t === "close") {
|
|
339
|
-
// The app owns its close chrome (its own top-right X). Tell the embedding host to
|
|
340
|
-
// dismiss the surface (twin of `unoverse:config`/`unoverse:size`). The host decides
|
|
341
|
-
// what "close" means for its frame (slide the drawer shut); we don't assume.
|
|
342
|
-
postToEmbedHost({ type: "unoverse:close" });
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (t === "sendMessage") {
|
|
346
|
-
// A suggestion that NAMES AN APP launches it instead of sending its text: read
|
|
347
|
-
// that app's manifest (the binding lives there — §4b, never in template data)
|
|
348
|
-
// and fire its trigger. The `text` field stays as the fallback for channels
|
|
349
|
-
// without launch support. Same-workflow apps only for now: the connection
|
|
350
|
-
// posts to the BOUND workflow id, so a foreign-workflow app logs and falls
|
|
351
|
-
// through to its trigger on the bound workflow.
|
|
352
|
-
const app = typeof (data as any)?.app === "string" ? (data as any).app : "";
|
|
353
|
-
if (app) {
|
|
354
|
-
(client as any).client
|
|
355
|
-
.readResource({ uri: `unoverse://apps/${app.toLowerCase()}` })
|
|
356
|
-
.then((res: any) => {
|
|
357
|
-
const parsed = res?.contents?.[0]?.text ? JSON.parse(res.contents[0].text) : null;
|
|
358
|
-
const b = parsed?.manifest?.binding ?? parsed?.binding ?? {};
|
|
359
|
-
if (!b.trigger) return console.error("[app] launch failed: no binding on", app);
|
|
360
|
-
if (b.workflow && binding.workflow && b.workflow !== binding.workflow)
|
|
361
|
-
console.warn("[app] launch:", app, "binds a different workflow — firing its trigger on the bound workflow");
|
|
362
|
-
// LOADING STATE: write the app's declared defaultState (open NAME; `mode` =
|
|
363
|
-
// pre-rename alias) into template state NOW — e.g. a "focus" app pre-opens
|
|
364
|
-
// the focus surface, whose slot shows its fallback (skeleton) until the
|
|
365
|
-
// workflow streams the component in. Same template-state key the node's
|
|
366
|
-
// APP_DATA emit writes; no new vocabulary. An "app" app IS the surface
|
|
367
|
-
// — nothing to write.
|
|
368
|
-
const declared = parsed?.manifest?.defaultState ?? parsed?.manifest?.mode ?? parsed?.defaultState ?? parsed?.mode;
|
|
369
|
-
if (declared && declared !== "app") store.mergeAppState({ defaultState: declared });
|
|
370
|
-
conn.trigger({ app, metadata: { targetTriggerNode: b.trigger, ...(def?.org ? { org: def.org } : {}) } });
|
|
371
|
-
})
|
|
372
|
-
.catch((e: any) => console.error("[app] launch failed:", e));
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
// Composer submit is generic: Enter carries `text`; a send BUTTON carries none,
|
|
376
|
-
// so fall back to the shared draft the store already holds (same as Studio).
|
|
377
|
-
const text = (typeof (data as any)?.text === "string" && (data as any).text) || store.getDraft();
|
|
378
|
-
if (text.trim()) {
|
|
379
|
-
conn.sendMessage(text);
|
|
380
|
-
store.setDraft(""); // clear the composer after sending (the Input binds `draft`)
|
|
381
|
-
}
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
// Controlled composer: the Input emits "input" on every keystroke → the shared
|
|
385
|
-
// draft (local, never a server round-trip). A send button / Enter emits "sendMessage".
|
|
386
|
-
if (t === "input") {
|
|
387
|
-
store.setDraft(typeof (data as any)?.text === "string" ? (data as any).text : "");
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
if (t) toServer(t, data, meta);
|
|
391
|
-
}}
|
|
392
|
-
/>
|
|
393
|
-
</>
|
|
394
|
-
);
|
|
395
|
-
}
|