@unoverse-platform/studio 0.1.103 → 0.1.104

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.
Files changed (79) hide show
  1. package/package.json +4 -2
  2. package/screens/AtomsView.tsx +2 -1
  3. package/screens/ComponentsView.tsx +2 -1
  4. package/screens/NativeAppHost.tsx +1 -2
  5. package/screens/TemplatesLibraryView.tsx +14 -1
  6. package/screens/TemplatesView.tsx +2 -1
  7. package/screens/index.ts +4 -0
  8. package/screens/react/unoverse.tsx +281 -0
  9. package/vendor/base/src/agent-mcp/index.ts +1 -1
  10. package/vendor/base/src/agent-mcp/invoke.ts +6 -2
  11. package/vendor/base/src/agent-mcp/tools.ts +14 -5
  12. package/vendor/base/src/agent-mcp/types.ts +6 -0
  13. package/vendor/base/src/auth/auth.ts +90 -23
  14. package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
  15. package/vendor/base/src/definitions/load/compile/template.ts +23 -2
  16. package/vendor/base/src/definitions/model.ts +9 -0
  17. package/vendor/base/src/definitions/registry/catalog.ts +6 -7
  18. package/vendor/base/src/executor.ts +25 -1
  19. package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
  20. package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
  21. package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
  22. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
  23. package/vendor/base/src/platform/serviceCalls.ts +13 -0
  24. package/vendor/sdk/appEngine.ts +363 -0
  25. package/vendor/sdk/lib/actions.ts +36 -0
  26. package/vendor/sdk/lib/citations.ts +48 -0
  27. package/vendor/sdk/lib/core/apps.ts +18 -0
  28. package/vendor/sdk/lib/core/client.ts +3 -3
  29. package/vendor/sdk/lib/core/store.ts +6 -0
  30. package/vendor/sdk/lib/dom/app.ts +546 -0
  31. package/vendor/sdk/lib/dom/chrome.ts +125 -0
  32. package/vendor/sdk/lib/dom/component.ts +333 -0
  33. package/vendor/sdk/lib/dom/connection.ts +309 -0
  34. package/vendor/sdk/lib/dom/element.ts +69 -0
  35. package/vendor/sdk/lib/dom/isolate.ts +37 -0
  36. package/vendor/sdk/lib/dom/markdown.ts +388 -0
  37. package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
  38. package/vendor/sdk/lib/dom/mounted.ts +29 -0
  39. package/vendor/sdk/lib/dom/primitives.ts +689 -0
  40. package/vendor/sdk/lib/dom/region.ts +195 -0
  41. package/vendor/sdk/lib/dom/render.ts +177 -0
  42. package/vendor/sdk/lib/dom/theme.ts +38 -0
  43. package/vendor/sdk/lib/dom/types.ts +36 -0
  44. package/vendor/sdk/lib/index.ts +26 -17
  45. package/vendor/sdk/lib/orb/canvas.ts +139 -0
  46. package/vendor/sdk/lib/orb/index.ts +8 -18
  47. package/vendor/sdk/lib/orb/presets.ts +1 -1
  48. package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
  49. package/vendor/sdk/lib/realtime/capture.ts +152 -0
  50. package/vendor/sdk/lib/realtime/playback.ts +129 -0
  51. package/vendor/sdk/lib/realtime/socket.ts +117 -0
  52. package/vendor/sdk/lib/style.ts +45 -37
  53. package/vendor/sdk/lib/voiceService.ts +344 -0
  54. package/vendor/sdk/main.ts +107 -0
  55. package/vendor/sdk/mcpAppBridge.ts +125 -102
  56. package/vendor/sdk/pageInterfaces.ts +452 -0
  57. package/vendor/sdk/responseView.ts +193 -0
  58. package/vite.config.ts +3 -3
  59. package/vendor/sdk/appEngine.tsx +0 -395
  60. package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
  61. package/vendor/sdk/lib/app.tsx +0 -730
  62. package/vendor/sdk/lib/connection.tsx +0 -433
  63. package/vendor/sdk/lib/isolate.tsx +0 -70
  64. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
  65. package/vendor/sdk/lib/orb/theme.ts +0 -83
  66. package/vendor/sdk/lib/primitives.tsx +0 -943
  67. package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
  68. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
  69. package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
  70. package/vendor/sdk/lib/render.tsx +0 -244
  71. package/vendor/sdk/lib/streamed.tsx +0 -30
  72. package/vendor/sdk/lib/theme.ts +0 -55
  73. package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
  74. package/vendor/sdk/lib/transition/index.ts +0 -9
  75. package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
  76. package/vendor/sdk/lib/voice.tsx +0 -422
  77. package/vendor/sdk/main.tsx +0 -125
  78. package/vendor/sdk/pageInterfaces.tsx +0 -388
  79. package/vendor/sdk/responseView.tsx +0 -82
@@ -1,388 +0,0 @@
1
- /**
2
- * THE PAGE'S OWN TEMPLATES (docs/unoverse/design/sdk/UNOVERSE_HOST_PAGE.md rule 3).
3
- *
4
- * A website that is the app marks where its unoverse templates go:
5
- *
6
- * <div data-unoverse-interface="unoverse/presence-avatar"></div>
7
- *
8
- * This runtime is already loaded (the embed started the app with no chrome). It reads each
9
- * marked template by URI over MCP, exactly as it reads the app, and draws it INTO the page's
10
- * element, in a shadow DOM (UnoverseComponent's own IsolatedRoot). One runtime, many shadow
11
- * roots: one client, one conversation, one sign-in for every part of the page.
12
- *
13
- * A template ARRIVES WITH ITS PAGE, as Studio previews it and as a delivery lands it: one
14
- * instance per linked part (`static:`), at its face, with the part's own values
15
- * (the served `sections` and their `seeds`), then the template itself. Drawn bare, its parts'
16
- * slots hold nothing and only the frame shows.
17
- *
18
- * The frame shares the page's origin (EMBED_CLIENT.md §1), so the page's document is
19
- * reachable from here; React portals listen for events on their own container, so a part
20
- * drawn into the page is as live as one drawn in the frame.
21
- */
22
- import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
23
- import { createPortal } from "react-dom";
24
- import { ComponentStore, UnoverseComponent, type ActionHandler, type ActionMeta, type UnoverseClient, type UnoverseDefinition, type VoiceService } from "./lib";
25
-
26
- const MARK = "data-unoverse-interface";
27
- const CHAT = "page";
28
- const TEMPLATE_NODE = "~template";
29
-
30
- /** The page this runtime serves, or null when there is none it may reach (an MCP host). */
31
- function pageDocument(): Document | null {
32
- try {
33
- return window.parent !== window ? window.parent.document : null;
34
- } catch {
35
- return null; // a cross-origin host: there is no page of ours to draw into
36
- }
37
- }
38
-
39
- /**
40
- * WHICH TEMPLATE THE ARROW KEYS BELONG TO: exactly one. The one the visitor last pressed in;
41
- * before any press, the one taking the most of the screen. Every template's state is its
42
- * own, so a key must never move two (the presentation's slides moved the avatar's steps).
43
- */
44
- let engaged: HTMLElement | null = null;
45
- function keyOwner(doc: Document): HTMLElement | null {
46
- const full = doc.fullscreenElement as HTMLElement | null;
47
- if (full?.hasAttribute(MARK)) return full; // a fullscreen template is the only one on screen
48
- if (engaged && doc.contains(engaged)) return engaged;
49
- const vh = doc.defaultView?.innerHeight ?? 0;
50
- let best: HTMLElement | null = null;
51
- let area = 0;
52
- for (const el of doc.querySelectorAll<HTMLElement>(`[${MARK}]`)) {
53
- const r = el.getBoundingClientRect();
54
- const seen = Math.max(0, Math.min(r.bottom, vh) - Math.max(r.top, 0)) * r.width;
55
- if (seen > area) (area = seen), (best = el);
56
- }
57
- return best;
58
- }
59
-
60
- /** A page template's address, from the name its element is marked with. */
61
- const uriOf = (name: string) => (name.includes("://") ? name : `unoverse://templates/${name}`);
62
-
63
- /** THE AVATARS: page templates whose steps are the call's phases (`listening`, `speaking`).
64
- * They are how the page is spoken to, so they are never the task in view; they are the ones
65
- * that follow the call. */
66
- const avatars = new Set<HTMLElement>();
67
- const CALL_PHASES = ["listening", "speaking"];
68
-
69
- /** The workflow a template names in its manifest (UNOVERSE_HOST_PAGE.md rule 4). */
70
- export type Binding = { workflow?: string; trigger?: string };
71
-
72
- /** THE TASK IN VIEW: the marked template, not an avatar, spanning the middle of the screen. */
73
- function taskInView(doc: Document): HTMLElement | null {
74
- const mid = (doc.defaultView?.innerHeight ?? 0) / 2;
75
- for (const el of doc.querySelectorAll<HTMLElement>(`[${MARK}]`)) {
76
- if (avatars.has(el)) continue;
77
- const r = el.getBoundingClientRect();
78
- if (r.top <= mid && r.bottom >= mid) return el;
79
- }
80
- return null;
81
- }
82
-
83
- /**
84
- * THE AVATAR'S SPOT WHILE A TEMPLATE IS FULLSCREEN. The browser shows only the fullscreen
85
- * element, so the avatar's own element (in the site's navigation) is hidden. A fullscreen
86
- * state's layout carries a `Place`; it draws this element, and the page's avatar draws there
87
- * until the place goes. It is the same avatar, so it keeps following the call.
88
- */
89
- let dock: HTMLElement | null = null;
90
- const dockWatchers = new Set<() => void>();
91
- const setDock = (at: HTMLElement | null) => {
92
- if (at === dock) return;
93
- dock = at;
94
- dockWatchers.forEach((f) => f());
95
- };
96
- const watchDock = (f: () => void) => {
97
- dockWatchers.add(f);
98
- return () => void dockWatchers.delete(f);
99
- };
100
- const drawDock = () => <div ref={setDock} />;
101
-
102
- type Section = { kind?: string; face?: string; ref?: string; preview?: string[]; seeds?: Record<string, Record<string, unknown>> };
103
-
104
- /** One marked template: read it, give it its parts, draw it. */
105
- function PageTemplate({ client, uri, el, voice, waiting, onBinding, onCall, appStore }: { client: UnoverseClient; uri: string; el: HTMLElement; voice: VoiceService; waiting: boolean; onBinding: (uri: string, binding: Binding) => void; onCall: ActionHandler; appStore: ComponentStore }) {
106
- const store = useMemo(() => new ComponentStore(), [uri]);
107
- const [ready, setReady] = useState(false);
108
- const partsRef = useRef<string[]>([]);
109
- const defRef = useRef<{ stateTree?: { name: string; on?: string; initial?: string; states?: { name: string }[]; fullscreen?: boolean }[] } | null>(null);
110
-
111
- useEffect(() => {
112
- let alive = true;
113
- client
114
- .readDefinition(uri)
115
- .then((def) => {
116
- if (!alive || !def) return;
117
- const d = def as UnoverseDefinition & { defaultLayout?: string; layouts?: Record<string, unknown>; sections?: Record<string, Section[]>; binding?: Binding };
118
- if (d.binding?.workflow) onBinding(uri, d.binding);
119
- defRef.current = d as typeof defRef.current;
120
- const base = d.defaultLayout ?? (d.layouts ? Object.keys(d.layouts)[0] : undefined);
121
- partsRef.current = [];
122
- for (const sec of (base && d.sections?.[base]) || []) {
123
- const parts = sec.ref ? [sec.ref] : sec.kind === "director" ? sec.preview ?? [] : [];
124
- parts.forEach((ref, i) => {
125
- const nodeId = `${sec.face}~${ref}~${i}`;
126
- partsRef.current.push(nodeId);
127
- store.apply({ type: "COMPONENT_INIT", chatId: CHAT, nodeId, component: { type: ref, props: sec.seeds?.[ref] ?? {} } });
128
- if (sec.face) store.mergeComponentState(CHAT, nodeId, { state: sec.face });
129
- });
130
- }
131
- const ref = uri.replace(/^unoverse:\/\/templates\//, "");
132
- store.apply({ type: "COMPONENT_INIT", chatId: CHAT, nodeId: TEMPLATE_NODE, component: { type: ref, componentUrl: uri, props: {} } });
133
- if (base) store.mergeComponentState(CHAT, TEMPLATE_NODE, { state: base });
134
- if ((d.stateTree ?? []).some((t) => CALL_PHASES.every((p) => t.states?.some((x) => x.name === p)))) avatars.add(el);
135
- setReady(true);
136
- })
137
- .catch((e) => console.warn(`[unoverse] page template "${uri}" failed to load:`, e));
138
- return () => {
139
- alive = false;
140
- };
141
- }, [client, uri, store]);
142
-
143
- // A TEMPLATE FOLLOWS ITS TASK. Whatever runs the task publishes where it is as
144
- // `tasks.<its URI>` in the app state (SIGNAL.md 6c); this template moves to that step, the
145
- // same write its own Next makes, whether the move came from a voice or a chat.
146
- // Its `props` too, keyed by part: `{ <part>: values }` merges into that part (a template's
147
- // content lives in its parts), once per change, so a template already on the page is
148
- // hydrated in place.
149
- //
150
- // THE SAME TEMPLATE, STREAMED. When a run renders this template, it streams into the
151
- // client's store like any interface. It is already on the page, so it is not drawn again:
152
- // this copy hydrates from it, each part's values onto that part. A copy still on its first
153
- // step (the wait, such as `finding`) moves to the next step as its values land; nothing
154
- // outside the template moves it.
155
- useEffect(() => {
156
- if (!ready) return;
157
- let lastProps = "";
158
- let lastStreamed = "";
159
- const fillParts = (parts: Record<string, unknown>) => {
160
- for (const [part, values] of Object.entries(parts)) {
161
- if (!values || typeof values !== "object" || Array.isArray(values)) continue;
162
- for (const nodeId of partsRef.current.filter((id) => id.startsWith(`${part}~`))) store.mergeComponentState(CHAT, nodeId, values as Record<string, unknown>);
163
- }
164
- };
165
- const moveTo = (at: unknown) => {
166
- if (typeof at !== "string") return;
167
- const tree = defRef.current?.stateTree?.find((t) => t.on && t.states?.some((x) => x.name === at));
168
- if (!tree?.on) return;
169
- const tpl = store.get(CHAT, TEMPLATE_NODE) as Record<string, unknown> | undefined;
170
- if (tpl?.[tree.on] !== at) store.mergeComponentState(CHAT, TEMPLATE_NODE, { [tree.on]: at });
171
- };
172
- const follow = () => {
173
- const task = ((appStore.getAppState().tasks ?? {}) as Record<string, { step?: string; props?: Record<string, unknown> }>)[uri];
174
- if (task?.props && typeof task.props === "object") {
175
- const next = JSON.stringify(task.props);
176
- if (next !== lastProps) {
177
- lastProps = next;
178
- fillParts(task.props);
179
- }
180
- }
181
- moveTo(task?.step);
182
- for (const r of appStore.getResponses()) {
183
- for (const pointer of r.components) {
184
- const { chatId, nodeId } = appStore.split(pointer);
185
- if ((appStore.getComponentUrl(chatId, nodeId) ?? "").split("#")[0] !== uri) continue;
186
- const streamed = appStore.get(chatId, nodeId) ?? {};
187
- const next = JSON.stringify(streamed);
188
- if (next === lastStreamed) continue;
189
- lastStreamed = next;
190
- if (!streamed.state || typeof streamed.state !== "object") continue;
191
- fillParts(streamed.state as Record<string, unknown>);
192
- const tree = defRef.current?.stateTree?.find((t) => t.on && (t.states?.length ?? 0) > 1);
193
- const tpl = store.get(CHAT, TEMPLATE_NODE) as Record<string, unknown> | undefined;
194
- if (tree?.on && (tpl?.[tree.on] ?? tree.initial) === tree.initial) moveTo(tree.states![1].name);
195
- }
196
- }
197
- };
198
- follow();
199
- return appStore.subscribe(follow);
200
- }, [ready, appStore, store, uri]);
201
-
202
- // THE ARROW KEYS STEP IT, on the PAGE. Left and Right move a template's steps (the served
203
- // step tree: `on` + its substates), the same write its own Back and Next make. The keys are
204
- // the page's, so they are heard on the page's document, not this frame's. Typing in a field
205
- // never steps, and only the template that owns the keys moves (keyOwner).
206
- useEffect(() => {
207
- const doc = pageDocument();
208
- if (!doc || !ready) return;
209
- const onKey = (e: KeyboardEvent) => {
210
- if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
211
- if (keyOwner(doc) !== el) return;
212
- const t = e.target as HTMLElement | null;
213
- if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
214
- const tpl = store.get(CHAT, TEMPLATE_NODE) as Record<string, unknown> | undefined;
215
- const def = defRef.current;
216
- const face = String(tpl?.state ?? "");
217
- const tree = def?.stateTree?.find((s) => s.name === face);
218
- const steps = tree?.states?.map((s) => s.name) ?? [];
219
- if (!tree?.on || steps.length < 2) return;
220
- const cur = String(tpl?.[tree.on] ?? tree.initial ?? steps[0]);
221
- const i = steps.indexOf(cur) + (e.key === "ArrowRight" ? 1 : -1);
222
- if (i < 0 || i >= steps.length) return;
223
- e.preventDefault();
224
- store.mergeComponentState(CHAT, TEMPLATE_NODE, { [tree.on]: steps[i] });
225
- };
226
- doc.addEventListener("keydown", onKey);
227
- return () => doc.removeEventListener("keydown", onKey);
228
- }, [ready, store, el]);
229
-
230
- // FULLSCREEN IS A STATE. Entering a state marked `fullscreen` takes this template's element
231
- // fullscreen; leaving it hands the screen back. The request is made inside the store write
232
- // the visitor's click made (setValue notifies synchronously), because a browser grants
233
- // fullscreen only to a click. Refused, or left with Esc, the template goes back to the
234
- // state it came from. The step is one value across states, so the slide stays put.
235
- useEffect(() => {
236
- const doc = pageDocument();
237
- if (!doc || !ready) return;
238
- const states = () => defRef.current?.stateTree ?? [];
239
- const isFull = (name: string) => states().some((t) => t.name === name && t.fullscreen);
240
- const face = () => String((store.get(CHAT, TEMPLATE_NODE) as Record<string, unknown> | undefined)?.state ?? "");
241
- let from = isFull(face()) ? (states().find((t) => !t.fullscreen)?.name ?? "") : face();
242
- let asking = false;
243
- const back = () => store.mergeComponentState(CHAT, TEMPLATE_NODE, { state: from });
244
- const sync = () => {
245
- const now = face();
246
- if (!isFull(now)) {
247
- from = now;
248
- if (doc.fullscreenElement === el) void doc.exitFullscreen();
249
- return;
250
- }
251
- if (doc.fullscreenElement === el || asking) return;
252
- asking = true;
253
- el.requestFullscreen()
254
- .catch(back)
255
- .finally(() => (asking = false));
256
- };
257
- const onChange = () => {
258
- if (doc.fullscreenElement !== el && !asking && isFull(face())) back();
259
- };
260
- const stop = store.subscribe(sync);
261
- doc.addEventListener("fullscreenchange", onChange);
262
- return () => {
263
- stop();
264
- doc.removeEventListener("fullscreenchange", onChange);
265
- };
266
- }, [ready, store, el]);
267
-
268
- // WHILE A TEMPLATE IS FULLSCREEN, AN AVATAR DRAWS IN ITS PLACE (drawDock).
269
- const docked = useSyncExternalStore(watchDock, () => dock);
270
-
271
- // AN AVATAR FOLLOWS THE CALL: its steps are the call's phases, so its step is the call's
272
- // state. `waiting` rides beside it, on any step: a task in view that the call is not working
273
- // yet. The avatar draws it as a pulse, and a press on the pulse starts or moves the call.
274
- const callState = voice.state.callState;
275
- useEffect(() => {
276
- if (!ready || !avatars.has(el)) return;
277
- const tree = defRef.current?.stateTree?.find((t) => CALL_PHASES.every((p) => t.states?.some((x) => x.name === p)));
278
- if (!tree?.on) return;
279
- store.mergeComponentState(CHAT, TEMPLATE_NODE, { [tree.on]: callState, waiting });
280
- }, [ready, store, el, callState, waiting]);
281
-
282
- if (!ready) return null;
283
- const drawn = <UnoverseComponent client={client} store={store} chatId={CHAT} nodeId={TEMPLATE_NODE} onAction={onCall} place={drawDock} appStore={appStore} />;
284
- return docked && avatars.has(el) ? createPortal(drawn, docked) : drawn;
285
- }
286
-
287
- /**
288
- * ONE APP STATE FOR THE WHOLE PAGE. Each template keeps its own slice in its own store, and
289
- * all of them read and write the app's state in `appStore`, the store the call and the
290
- * workflow write to. A name one template saves is a name every other template can show.
291
- */
292
- export function PageInterfaces({ org, client, voice, appStore, toServer, startTask }: { org: string; client: UnoverseClient; voice: VoiceService; appStore: ComponentStore; toServer: (t: string, data: Record<string, unknown>, meta?: ActionMeta) => void; startTask: (binding: Binding, task: string, submit?: Record<string, unknown>) => void }) {
293
- const [targets, setTargets] = useState<HTMLElement[]>([]);
294
- // The task in view, by its URI, measured again as the page scrolls or resizes.
295
- const [inView, setInView] = useState<string | null>(null);
296
- // Each template's own workflow, as it loads; a template without one is never called.
297
- const [bindings, setBindings] = useState<Record<string, Binding>>({});
298
- const onBinding = useCallback((uri: string, b: Binding) => setBindings((prev) => (prev[uri] ? prev : { ...prev, [uri]: b })), []);
299
- // The tasks this page has started. Each starts once: a second press starts nothing. The ref
300
- // holds it at once, so two quick presses are still one; the state redraws the avatar.
301
- const started = useRef(new Set<string>());
302
- const [, setStarted] = useState(0);
303
-
304
- // THE AVATAR STARTS THE TASK'S WORKFLOW (UNOVERSE_HOST_PAGE.md rule 4), like any button. The
305
- // page connects to nothing until it is pressed. A task in view with a workflow, that is not
306
- // already running, is WAITING: a press starts its workflow at the template's trigger. A voice
307
- // in that workflow is the workflow's own. Nothing in view, or nothing bound: nothing.
308
- const target = inView && bindings[inView]?.workflow ? inView : null;
309
- const waiting = !!target && !started.current.has(target);
310
- // A TEMPLATE'S OWN ACTIONS go where the app's go: its `submit` answers whoever opened the task,
311
- // exactly as in the app (appEngine `toServer`). A template with its own workflow also has
312
- // that workflow called with the answers: the submit is the task's last call, and its Task
313
- // Runner finishes it (design/tasks.md, "The submit"). The call's three are the page's.
314
- const onCallFor = (uri: string): ActionHandler => (spec, data, meta) => {
315
- const action = typeof spec === "string" ? spec : spec?.type;
316
- if (action === "submit" || action === "cancel") {
317
- toServer(action, data, meta);
318
- const state = (meta?.state ?? data) as Record<string, unknown> | undefined;
319
- if (action === "submit" && bindings[uri]?.workflow && state && typeof state === "object") startTask(bindings[uri], uri, state);
320
- } else if (action === "startCall") {
321
- if (!target || started.current.has(target)) return;
322
- started.current.add(target);
323
- setStarted((n) => n + 1);
324
- // The press is the click the browser needs before it plays a voice the workflow may have.
325
- voice.unlock();
326
- startTask(bindings[target], target);
327
- } else if (action === "endCall") {
328
- void voice.endCall();
329
- } else if (action === "toggleMute") voice.toggleMute();
330
- // Another app on the page, opened or closed by its `<org>/<app>` (its own tag, EMBED_CLIENT.md).
331
- else if (action === "toggleApp") {
332
- const app = (data as { app?: unknown } | undefined)?.app;
333
- if (typeof app === "string") (pageDocument()?.defaultView as (Window & { unoverseApps?: Record<string, { toggle(): void }> }) | undefined)?.unoverseApps?.[app]?.toggle();
334
- }
335
- };
336
-
337
- useEffect(() => {
338
- const doc = pageDocument();
339
- const win = doc?.defaultView;
340
- if (!doc || !win) return;
341
- const measure = () => {
342
- const name = taskInView(doc)?.getAttribute(MARK);
343
- setInView(name ? uriOf(name) : null);
344
- };
345
- measure();
346
- win.addEventListener("scroll", measure, { passive: true });
347
- win.addEventListener("resize", measure);
348
- return () => {
349
- win.removeEventListener("scroll", measure);
350
- win.removeEventListener("resize", measure);
351
- };
352
- }, [targets]);
353
-
354
- useEffect(() => {
355
- const doc = pageDocument();
356
- if (!doc) return;
357
- const scan = () => {
358
- // Only this app's own org's templates: another app on the page draws its own.
359
- const found = [...doc.querySelectorAll<HTMLElement>(`[${MARK}]`)].filter((el) => el.getAttribute(MARK)?.startsWith(`${org}/`));
360
- setTargets((prev) => (prev.length === found.length && prev.every((el, i) => el === found[i]) ? prev : found));
361
- };
362
- scan();
363
- // The page may render its parts after this runtime starts (a client-rendered page),
364
- // so it keeps looking; only a change in the marked set re-renders.
365
- const watch = new MutationObserver(scan);
366
- watch.observe(doc.body, { childList: true, subtree: true });
367
- // A press anywhere inside a marked template makes it the one the keys move; a press
368
- // outside every template hands the keys back to the one most in view.
369
- const onPress = (e: Event) => {
370
- engaged = (e.composedPath().find((n) => n instanceof HTMLElement && n.hasAttribute(MARK)) as HTMLElement | undefined) ?? null;
371
- };
372
- doc.addEventListener("pointerdown", onPress, true);
373
- return () => {
374
- watch.disconnect();
375
- doc.removeEventListener("pointerdown", onPress, true);
376
- };
377
- }, []);
378
-
379
- return (
380
- <>
381
- {targets.map((el) => {
382
- const name = el.getAttribute(MARK) ?? "";
383
- if (!name) return null;
384
- return createPortal(<PageTemplate client={client} uri={uriOf(name)} el={el} voice={voice} waiting={waiting} onBinding={onBinding} onCall={onCallFor(uriOf(name))} appStore={appStore} />, el, name);
385
- })}
386
- </>
387
- );
388
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * THE RESPONSE VIEW — what an UNNAMED result renders (THE NAMING RULE,
3
- * docs/mcp/UNOVERSE_MCP_APP_PROTOCOL § THE NAMING RULE).
4
- *
5
- * An MCP Action's result carries `interface: { kind: "components", components }` —
6
- * the component events the run produced. This view renders EXACTLY those: seed a
7
- * fresh store with the events, resolve the theme, paint each instance with the
8
- * standard renderer. No app mount, no session registration, no home state, no
9
- * auto-trigger — an app mounts only when something NAMES it (a channel, a row, or
10
- * a result declaring `kind: "app"`).
11
- *
12
- * Definitions and theme still resolve over the result's serverUrl (`/mcp` reads) —
13
- * the response is data; the rendering vocabulary lives on the universe.
14
- */
15
- import { useMemo } from "react";
16
- import {
17
- UnoverseClient,
18
- ComponentStore,
19
- StreamedUnoverseComponent,
20
- useUnoverseTheme,
21
- applyServerMessage,
22
- } from "./lib";
23
-
24
- export interface ResponseViewProps {
25
- serverUrl: string;
26
- /** The run's component events (COMPONENT_INIT / COMPONENT_DATA), as collected
27
- * server-side and carried in the result's `interface.components`. */
28
- events: Record<string, unknown>[];
29
- }
30
-
31
- export function ResponseView({ serverUrl, events }: ResponseViewProps) {
32
- const client = useMemo(() => new UnoverseClient(`${serverUrl}/mcp`, {}), [serverUrl]);
33
-
34
- // Seed once per result: the same pure mapper the live stream uses, so a seeded
35
- // component is indistinguishable from a streamed one (merge-not-replace and all).
36
- const store = useMemo(() => {
37
- const s = new ComponentStore();
38
- const seen = new Set<string>();
39
- for (const e of events) {
40
- try {
41
- applyServerMessage(s, e as never, seen);
42
- } catch {
43
- /* one malformed event must not blank the whole response */
44
- }
45
- }
46
- return s;
47
- }, [events]);
48
-
49
- const theme = useUnoverseTheme(client);
50
-
51
- // One card per INSTANCE, in arrival order (the events carry chatId+nodeId — the
52
- // store's own keying — so no pointer parsing).
53
- const instances = useMemo(() => {
54
- const out: { chatId: string; nodeId: string }[] = [];
55
- const seen = new Set<string>();
56
- for (const e of events) {
57
- const ev = e as { type?: string; chatId?: string; nodeId?: string };
58
- if (ev.type !== "COMPONENT_INIT" || !ev.chatId || !ev.nodeId) continue;
59
- const key = `${ev.chatId}:${ev.nodeId}`;
60
- if (seen.has(key)) continue;
61
- seen.add(key);
62
- out.push({ chatId: ev.chatId, nodeId: ev.nodeId });
63
- }
64
- return out;
65
- }, [events]);
66
-
67
- if (!instances.length) return null;
68
- return (
69
- <div style={{ display: "flex", flexDirection: "column", gap: 12, padding: 12 }}>
70
- {instances.map((i) => (
71
- <StreamedUnoverseComponent
72
- key={`${i.chatId}:${i.nodeId}`}
73
- client={client}
74
- store={store}
75
- chatId={i.chatId}
76
- nodeId={i.nodeId}
77
- theme={theme ?? undefined}
78
- />
79
- ))}
80
- </div>
81
- );
82
- }