@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.
- 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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unoverse-platform/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.104",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
|
|
6
6
|
"scripts": {
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"@codemirror/state": "^6.5.2",
|
|
27
27
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
28
28
|
"@codemirror/view": "^6.37.1",
|
|
29
|
-
"@modelcontextprotocol/
|
|
29
|
+
"@modelcontextprotocol/client": "^2.2.0",
|
|
30
|
+
"@modelcontextprotocol/ext-apps": "^2.0.3",
|
|
31
|
+
"@modelcontextprotocol/server": "^2.2.0",
|
|
30
32
|
"@smithy/protocol-http": "^5.5.14",
|
|
31
33
|
"@smithy/signature-v4": "^5.6.10",
|
|
32
34
|
"@tailwindcss/vite": "^4.0.0",
|
package/screens/AtomsView.tsx
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* Meta (no manifest). Ported from apps/unoverse/web/studio (kept diffable).
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect, useMemo, useState, type MouseEvent as ReactMouseEvent } from "react";
|
|
10
|
-
import {
|
|
10
|
+
import type { ResolvedTheme } from "@unoverse/sdk";
|
|
11
|
+
import { UnoverseComponent } from "./react/unoverse";
|
|
11
12
|
import { client, authedFetch } from "studio-host";
|
|
12
13
|
import { usePersistentState } from "studio-host";
|
|
13
14
|
import { NAV, SCREEN, navItem } from "./chrome";
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
7
7
|
import { ComponentStore } from "@unoverse/sdk";
|
|
8
|
-
import {
|
|
8
|
+
import type { ResolvedTheme } from "@unoverse/sdk";
|
|
9
|
+
import { UnoverseComponent } from "./react/unoverse";
|
|
9
10
|
import { client, authedFetch, hasSourceAccess } from "studio-host";
|
|
10
11
|
import { SourceView } from "./SourceView";
|
|
11
12
|
import { stateActivation, faceOf, faceAxisOf, switchLayers } from "./states";
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
* required, from the thing that owns it. Nothing is vendored, nothing is bundled.
|
|
11
11
|
*/
|
|
12
12
|
import { useEffect, useRef, useState } from "react";
|
|
13
|
-
import { Client } from "@modelcontextprotocol/
|
|
14
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
13
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
15
14
|
|
|
16
15
|
const APP_UI_URI = "ui://unoverse/app.html";
|
|
17
16
|
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* NOT TemplatesView.tsx: that name is taken by the Apps screen (the legacy noun).
|
|
16
16
|
*/
|
|
17
17
|
import { useEffect, useMemo, useState, useSyncExternalStore, useCallback, type MouseEvent as ReactMouseEvent } from "react";
|
|
18
|
-
import { ComponentStore,
|
|
18
|
+
import { ComponentStore, type ResolvedTheme } from "@unoverse/sdk";
|
|
19
|
+
import { UnoverseComponent } from "./react/unoverse";
|
|
19
20
|
import { client, authedFetch, usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
20
21
|
import { NAV, SCREEN, navItem, navSubItem } from "./chrome";
|
|
21
22
|
|
|
@@ -53,6 +54,8 @@ interface TemplateDef {
|
|
|
53
54
|
defaultLayout?: string;
|
|
54
55
|
/** Seeding map the loader assembles: face → interface refs. */
|
|
55
56
|
preview?: Record<string, string[]>;
|
|
57
|
+
/** The components it holds, one stand-in per item of its own preview data, per state. */
|
|
58
|
+
heldSeeds?: Record<string, { key: string; component: string; props: Record<string, unknown> }[]>;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
const PREVIEW_CHAT = "preview";
|
|
@@ -185,6 +188,16 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
185
188
|
});
|
|
186
189
|
const base = current.defaultLayout ?? (current.layouts ? Object.keys(current.layouts)[0] : undefined);
|
|
187
190
|
if (base) store.mergeComponentState(PREVIEW_CHAT, TEMPLATE_NODE, { state: base });
|
|
191
|
+
// WHAT IT HOLDS (docs/design/templates.md "Holding a component"): each item its own
|
|
192
|
+
// instance under the template's node id, the id the SDK's held slot looks for. With no
|
|
193
|
+
// screen here, an opened one stays in its spot and draws its own page there.
|
|
194
|
+
const held = new Set<string>();
|
|
195
|
+
for (const seed of Object.values(current.heldSeeds ?? {}).flat()) {
|
|
196
|
+
const nodeId = `${TEMPLATE_NODE}:${seed.component}:${seed.key}`;
|
|
197
|
+
if (held.has(nodeId)) continue;
|
|
198
|
+
held.add(nodeId);
|
|
199
|
+
store.apply({ type: "COMPONENT_INIT", chatId: PREVIEW_CHAT, nodeId, component: { type: seed.component, props: seed.props } });
|
|
200
|
+
}
|
|
188
201
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
189
202
|
}, [store, current]);
|
|
190
203
|
|
|
@@ -23,7 +23,8 @@ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from
|
|
|
23
23
|
import { useAuth } from "react-oidc-context";
|
|
24
24
|
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
25
25
|
import { ComponentStore } from "@unoverse/sdk";
|
|
26
|
-
import {
|
|
26
|
+
import { computeAppWidth, resolveActiveLayout, visible, type ResolvedTheme } from "@unoverse/sdk";
|
|
27
|
+
import { StreamedUnoverseApp, useUnoverseConnection, type UseVoiceServiceConfig } from "./react/unoverse";
|
|
27
28
|
import { NativeAppHost } from "./NativeAppHost";
|
|
28
29
|
import { inArrangement, viewOfSlice } from "./states";
|
|
29
30
|
import { client, authedFetch, hasAuth, hasSourceAccess, UNOVERSE_BASE, liveUniverseUrl } from "studio-host";
|
package/screens/index.ts
CHANGED
|
@@ -37,3 +37,7 @@ export { ConfigForm, defaultsFromSchema } from "./ConfigForm";
|
|
|
37
37
|
// The source view renders only where the host grants source access, so it is exported
|
|
38
38
|
// unconditionally and returns null in Canvas (docs/architecture/authoring/LOCAL_STUDIO.md).
|
|
39
39
|
export { SourceView } from "./SourceView";
|
|
40
|
+
// The React faces of the plain SDK (the SDK has no React): one component, an app, the live
|
|
41
|
+
// connection and the voice service, for the React hosts that share these screens.
|
|
42
|
+
export { UnoverseComponent, StreamedUnoverseApp, useUnoverseConnection, useVoiceService } from "./react/unoverse";
|
|
43
|
+
export type { UnoverseComponentProps, StreamedUnoverseAppProps, UnoverseConnection, UseVoiceServiceConfig, VoiceService } from "./react/unoverse";
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE REACT ADAPTER for the Unoverse SDK, for the React apps that host it (Studio, the canvas).
|
|
3
|
+
*
|
|
4
|
+
* The SDK has no React (apps/unoverse/web/sdk/CLAUDE.md): it draws with `mountComponent`,
|
|
5
|
+
* `mountApp`, `openConnection` and `createVoiceService`. These four names give a React tree the
|
|
6
|
+
* same props it always passed, each a thin mount of the plain API into one element. Nothing here
|
|
7
|
+
* draws or decides: the SDK does (docs/unoverse/design/sdk/UNOVERSE_SDK_NO_REACT.md §6 (a)).
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from "react";
|
|
10
|
+
import { createPortal } from "react-dom";
|
|
11
|
+
import {
|
|
12
|
+
mountComponent,
|
|
13
|
+
mountApp,
|
|
14
|
+
openConnection,
|
|
15
|
+
createVoiceService,
|
|
16
|
+
sessionUserId,
|
|
17
|
+
type ActionHandler,
|
|
18
|
+
type AppMount,
|
|
19
|
+
type ComponentMount,
|
|
20
|
+
type ComponentStore,
|
|
21
|
+
type ResolvedTheme,
|
|
22
|
+
type UnoverseClient,
|
|
23
|
+
type UnoverseConnection as PlainConnection,
|
|
24
|
+
type UnoverseConnectionConfig,
|
|
25
|
+
type UnoverseSessionParams,
|
|
26
|
+
type VoiceService as PlainVoiceService,
|
|
27
|
+
type VoiceServiceConfig,
|
|
28
|
+
type VoiceServiceState,
|
|
29
|
+
} from "@unoverse/sdk";
|
|
30
|
+
|
|
31
|
+
/** One element the plain mount draws into; `display: contents` keeps it out of layout. */
|
|
32
|
+
function useHost() {
|
|
33
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
34
|
+
return { ref, el: () => ref.current };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface UnoverseComponentProps {
|
|
38
|
+
client: UnoverseClient;
|
|
39
|
+
uri?: string;
|
|
40
|
+
data?: Record<string, unknown>;
|
|
41
|
+
store?: ComponentStore;
|
|
42
|
+
chatId?: string;
|
|
43
|
+
nodeId?: string;
|
|
44
|
+
extraData?: Record<string, unknown>;
|
|
45
|
+
onAction?: ActionHandler;
|
|
46
|
+
theme?: ResolvedTheme;
|
|
47
|
+
isolate?: boolean;
|
|
48
|
+
fill?: boolean;
|
|
49
|
+
/** What a `Place` in the definition draws (React content, drawn into the place). */
|
|
50
|
+
place?: (name: string) => ReactNode;
|
|
51
|
+
appStore?: ComponentStore;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** React content the plain SDK draws into an element it is handed (a place, a chrome). */
|
|
55
|
+
function usePortals() {
|
|
56
|
+
const holders = useRef(new Map<string, { el: HTMLDivElement; node: ReactNode; sig: string }>());
|
|
57
|
+
const [, redraw] = useState(0);
|
|
58
|
+
/** The element for `key`, with `node` to draw in it; redraws only when `sig` changes. */
|
|
59
|
+
const hold = useCallback((key: string, node: ReactNode, sig: string) => {
|
|
60
|
+
let h = holders.current.get(key);
|
|
61
|
+
if (!h) {
|
|
62
|
+
h = { el: document.createElement("div"), node, sig };
|
|
63
|
+
h.el.style.display = "contents";
|
|
64
|
+
holders.current.set(key, h);
|
|
65
|
+
queueMicrotask(() => redraw((n) => n + 1));
|
|
66
|
+
} else {
|
|
67
|
+
h.node = node;
|
|
68
|
+
if (h.sig !== sig) {
|
|
69
|
+
h.sig = sig;
|
|
70
|
+
queueMicrotask(() => redraw((n) => n + 1));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return h.el;
|
|
74
|
+
}, []);
|
|
75
|
+
const portals = [...holders.current.entries()].map(([key, h]) => createPortal(h.node, h.el, key));
|
|
76
|
+
return { hold, portals };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function UnoverseComponent(props: UnoverseComponentProps) {
|
|
80
|
+
const host = useHost();
|
|
81
|
+
const mount = useRef<ComponentMount | null>(null);
|
|
82
|
+
const { hold, portals } = usePortals();
|
|
83
|
+
const latest = useRef(props);
|
|
84
|
+
latest.current = props;
|
|
85
|
+
const options = () => {
|
|
86
|
+
const p = latest.current;
|
|
87
|
+
return {
|
|
88
|
+
client: p.client as never,
|
|
89
|
+
uri: p.uri,
|
|
90
|
+
data: p.data,
|
|
91
|
+
store: p.store,
|
|
92
|
+
chatId: p.chatId,
|
|
93
|
+
nodeId: p.nodeId,
|
|
94
|
+
extraData: p.extraData,
|
|
95
|
+
onAction: p.onAction,
|
|
96
|
+
theme: p.theme,
|
|
97
|
+
isolate: p.isolate,
|
|
98
|
+
fill: p.fill,
|
|
99
|
+
appStore: p.appStore,
|
|
100
|
+
place: p.place ? (name: string) => [hold(`place:${name}`, latest.current.place?.(name), name)] : undefined,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
useLayoutEffect(() => {
|
|
104
|
+
const m = mountComponent(options());
|
|
105
|
+
mount.current = m;
|
|
106
|
+
host.el()?.append(...m.nodes);
|
|
107
|
+
return () => {
|
|
108
|
+
m.destroy();
|
|
109
|
+
m.nodes.forEach((n) => n.parentNode?.removeChild(n));
|
|
110
|
+
mount.current = null;
|
|
111
|
+
};
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one mount; props flow through update
|
|
113
|
+
}, []);
|
|
114
|
+
useLayoutEffect(() => {
|
|
115
|
+
mount.current?.update(options());
|
|
116
|
+
});
|
|
117
|
+
return (
|
|
118
|
+
<div ref={host.ref} style={{ display: "contents" }}>
|
|
119
|
+
{portals}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface StreamedUnoverseAppProps {
|
|
125
|
+
client: UnoverseClient;
|
|
126
|
+
store: ComponentStore;
|
|
127
|
+
uri: string;
|
|
128
|
+
onAction?: ActionHandler;
|
|
129
|
+
theme?: ResolvedTheme;
|
|
130
|
+
props?: Record<string, unknown>;
|
|
131
|
+
isolate?: boolean;
|
|
132
|
+
/** DEV CHROME ON A CONTAINER: React content drawn above a Container's content. */
|
|
133
|
+
templateChrome?: (info: { name: string; states: string[]; active?: string; set: (state: string) => void }) => ReactNode;
|
|
134
|
+
/** The voice service config, for a `service: "voice"` app (the SDK starts it). */
|
|
135
|
+
voice?: VoiceServiceConfig;
|
|
136
|
+
/** The voice service, when the host already runs one. */
|
|
137
|
+
voiceService?: Pick<PlainVoiceService, "startCall" | "endCall" | "toggleMute">;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function StreamedUnoverseApp(props: StreamedUnoverseAppProps) {
|
|
141
|
+
const host = useHost();
|
|
142
|
+
const mount = useRef<AppMount | null>(null);
|
|
143
|
+
const { hold, portals } = usePortals();
|
|
144
|
+
const latest = useRef(props);
|
|
145
|
+
latest.current = props;
|
|
146
|
+
// The app's own voice service, when given a config and no service.
|
|
147
|
+
const own = useMemo(
|
|
148
|
+
() => (props.voice && !props.voiceService ? createVoiceService({ ...props.voice, wsUrl: props.client.serverOrigin.replace(/^http/, "ws") }) : null),
|
|
149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one per config identity
|
|
150
|
+
[props.voice, props.voiceService, props.client],
|
|
151
|
+
);
|
|
152
|
+
useEffect(() => () => own?.destroy(), [own]);
|
|
153
|
+
const options = () => {
|
|
154
|
+
const p = latest.current;
|
|
155
|
+
return {
|
|
156
|
+
client: p.client as never,
|
|
157
|
+
store: p.store,
|
|
158
|
+
uri: p.uri,
|
|
159
|
+
onAction: p.onAction,
|
|
160
|
+
theme: p.theme,
|
|
161
|
+
props: p.props,
|
|
162
|
+
isolate: p.isolate,
|
|
163
|
+
voice: p.voiceService ?? own ?? undefined,
|
|
164
|
+
templateChrome: p.templateChrome
|
|
165
|
+
? (info: { name: string; states: string[]; active?: string; set: (state: string) => void }) =>
|
|
166
|
+
hold(`chrome:${info.name}`, latest.current.templateChrome?.(info), JSON.stringify([info.states, info.active]))
|
|
167
|
+
: undefined,
|
|
168
|
+
doc: document,
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
useLayoutEffect(() => {
|
|
172
|
+
const m = mountApp(options());
|
|
173
|
+
mount.current = m;
|
|
174
|
+
host.el()?.append(...m.nodes);
|
|
175
|
+
return () => {
|
|
176
|
+
m.destroy();
|
|
177
|
+
m.nodes.forEach((n) => n.parentNode?.removeChild(n));
|
|
178
|
+
mount.current = null;
|
|
179
|
+
};
|
|
180
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- one mount; props flow through update
|
|
181
|
+
}, []);
|
|
182
|
+
useLayoutEffect(() => {
|
|
183
|
+
mount.current?.update(options());
|
|
184
|
+
});
|
|
185
|
+
return (
|
|
186
|
+
<div ref={host.ref} style={{ display: "contents" }}>
|
|
187
|
+
{portals}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface UnoverseConnection {
|
|
193
|
+
isConnected: boolean;
|
|
194
|
+
isReady: boolean;
|
|
195
|
+
refusal: string | null;
|
|
196
|
+
sendMessage: (text: string) => void;
|
|
197
|
+
trigger: (input?: Record<string, unknown>) => void;
|
|
198
|
+
sendAction: (action: string, data: Record<string, unknown>) => void;
|
|
199
|
+
resolveElicitation: (state: Record<string, unknown>, action?: "accept" | "cancel") => boolean;
|
|
200
|
+
/** The workflow-selected template (MCP app) name, or null. */
|
|
201
|
+
activeApp: string | null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const IDLE = { isConnected: false, isReady: false, refusal: null };
|
|
205
|
+
|
|
206
|
+
/** The live connection, opened for a React host and re-rendered on its status. */
|
|
207
|
+
export function useUnoverseConnection(
|
|
208
|
+
config: UnoverseConnectionConfig,
|
|
209
|
+
session: UnoverseSessionParams,
|
|
210
|
+
store: ComponentStore,
|
|
211
|
+
options: { enabled?: boolean; onAppSwitch?: (app: string) => void; onJoinCall?: (workflowId?: string) => void } = {},
|
|
212
|
+
): UnoverseConnection {
|
|
213
|
+
const enabled = options.enabled ?? true;
|
|
214
|
+
// Read through refs, so a host's inline arrow or a new app never tears the stream down.
|
|
215
|
+
const callbacks = useRef(options);
|
|
216
|
+
callbacks.current = options;
|
|
217
|
+
const appId = useRef(session.appId);
|
|
218
|
+
appId.current = session.appId;
|
|
219
|
+
const userId = sessionUserId(session.userId, config.getAccessToken);
|
|
220
|
+
const chatId = session.chatId ?? session.conversationId;
|
|
221
|
+
const [conn, setConn] = useState<PlainConnection | null>(null);
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (!enabled) return; // mock mode: no live stream
|
|
224
|
+
const c = openConnection(config, session, store, {
|
|
225
|
+
onAppSwitch: (app) => callbacks.current.onAppSwitch?.(app),
|
|
226
|
+
onJoinCall: (w) => callbacks.current.onJoinCall?.(w),
|
|
227
|
+
appId: () => appId.current,
|
|
228
|
+
});
|
|
229
|
+
setConn(c);
|
|
230
|
+
return () => {
|
|
231
|
+
c.close();
|
|
232
|
+
setConn(null);
|
|
233
|
+
};
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the fields the connection reads
|
|
235
|
+
}, [enabled, config.streamUrl, session.workflowId, session.targetTriggerNode, userId, session.conversationId, chatId, config.getAccessToken, store]);
|
|
236
|
+
const status = useSyncExternalStore(
|
|
237
|
+
useCallback((cb: () => void) => (conn ? conn.subscribe(cb) : () => {}), [conn]),
|
|
238
|
+
useCallback(() => conn?.status() ?? IDLE, [conn]),
|
|
239
|
+
);
|
|
240
|
+
const activeApp = useSyncExternalStore(
|
|
241
|
+
useCallback((cb: () => void) => store.subscribe(cb), [store]),
|
|
242
|
+
useCallback(() => store.getActiveApp().name, [store]),
|
|
243
|
+
);
|
|
244
|
+
// Without a connection (mock mode, or before it opens) a send still shows the person's turn
|
|
245
|
+
// and the thinking state; the run itself is dropped, loudly.
|
|
246
|
+
const sendMessage = useCallback(
|
|
247
|
+
(text: string) => {
|
|
248
|
+
if (conn) return conn.sendMessage(text);
|
|
249
|
+
const message = text.trim();
|
|
250
|
+
if (!message) return;
|
|
251
|
+
const turn = `${chatId}:t${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36)}`;
|
|
252
|
+
store.addUserMessage(turn, message);
|
|
253
|
+
store.startResponse(turn);
|
|
254
|
+
console.error("[unoverse] trigger dropped — MCP stream not connected yet");
|
|
255
|
+
},
|
|
256
|
+
[conn, chatId, store],
|
|
257
|
+
);
|
|
258
|
+
const trigger = useCallback((input?: Record<string, unknown>) => (conn ? conn.trigger(input) : console.error("[unoverse] trigger dropped — MCP stream not connected yet")), [conn]);
|
|
259
|
+
const sendAction = useCallback((action: string, data: Record<string, unknown>) => conn?.sendAction(action, data), [conn]);
|
|
260
|
+
const resolveElicitation = useCallback((state: Record<string, unknown>, action: "accept" | "cancel" = "accept") => conn?.resolveElicitation(state, action) ?? false, [conn]);
|
|
261
|
+
return { ...status, sendMessage, trigger, sendAction, resolveElicitation, activeApp };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type UseVoiceServiceConfig = VoiceServiceConfig;
|
|
265
|
+
export type VoiceService = Omit<PlainVoiceService, "subscribe" | "destroy" | "state"> & { state: VoiceServiceState };
|
|
266
|
+
|
|
267
|
+
/** The voice service, held for a React host and re-rendered on its state. */
|
|
268
|
+
export function useVoiceService(config: UseVoiceServiceConfig): VoiceService {
|
|
269
|
+
const { session } = config;
|
|
270
|
+
const service = useMemo(
|
|
271
|
+
() => createVoiceService(config),
|
|
272
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on what the service reads
|
|
273
|
+
[config.apiUrl, config.wsUrl, config.getAccessToken, config.store, session.userId, session.conversationId, session.chatId, session.workflowId, session.targetTriggerNode],
|
|
274
|
+
);
|
|
275
|
+
useEffect(() => () => service.destroy(), [service]);
|
|
276
|
+
const state = useSyncExternalStore(
|
|
277
|
+
(cb) => service.subscribe(cb),
|
|
278
|
+
() => service.state,
|
|
279
|
+
);
|
|
280
|
+
return { state, startCall: service.startCall, joinCall: service.joinCall, endCall: service.endCall, toggleMute: service.toggleMute, unlock: service.unlock };
|
|
281
|
+
}
|
|
@@ -38,7 +38,7 @@ export { parseToolResult, isTurnEndingHandoff } from "./handoff.js";
|
|
|
38
38
|
export { rowComponentsFromResults, renderRowComponents, rankedRowIds, componentTypeFromAppUri, onScreenRead, onScreenPlaces, placesOn, placesLine, rememberRows, rowsById, cardIdOf, screenOf, openedOn, type ScreenItem, openTasksOn, onScreenLine, ABOUT_THE_PAGE, ABOUT_THE_OPENED } from "./rowComponents.js";
|
|
39
39
|
export { leanToolResultForModel } from "./lean.js";
|
|
40
40
|
export { toolDefFromDiscoveredMCP, resolveToolDefNative, isBindinglessComponentApp, componentAppInvoker, isWorkflowTask, workflowTaskInvoker, workflowAppInvoker, modelToolName, objectFunctionToolDef, objectFunctionInvoker } from "./tools.js";
|
|
41
|
-
export { invokeTaskNative, connectPlatformMcp } from "./invoke.js";
|
|
41
|
+
export { invokeTaskNative, connectPlatformMcp, TRACE_META } from "./invoke.js";
|
|
42
42
|
|
|
43
43
|
export { ingestConversationTurn, type ConversationTurn } from "./memoryIngest.js";
|
|
44
44
|
export { fetchUserMemoryContext, hydratePromptWithUserMemory } from "./memoryContext.js";
|
|
@@ -31,13 +31,16 @@ import type { AppInvocationContext, DiscoveredMCP } from "./types.js";
|
|
|
31
31
|
* harness (every agent family) using the app id it parsed at mint time — so it needs NO
|
|
32
32
|
* server-side (XState) schema cache.
|
|
33
33
|
*/
|
|
34
|
+
/** The `_meta` key a platform call carries its caller under (AppInvocationContext.trace). */
|
|
35
|
+
export const TRACE_META = "unoverse/trace";
|
|
36
|
+
|
|
34
37
|
/** Open an MCP client on the platform's internal `/mcp` — the ONE wire-connect shared by
|
|
35
38
|
* every native call this harness makes (app invocation, live schema read). */
|
|
36
39
|
export async function connectPlatformMcp(
|
|
37
40
|
ctx: Pick<AppInvocationContext, "accessToken" | "baseUrl">,
|
|
38
41
|
): Promise<{ client: any; close: () => Promise<void> }> {
|
|
39
|
-
const { Client } = await import("@modelcontextprotocol/
|
|
40
|
-
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/
|
|
42
|
+
const { Client } = await import("@modelcontextprotocol/client");
|
|
43
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/client");
|
|
41
44
|
|
|
42
45
|
const base = (ctx.baseUrl || process.env.UNOVERSE_SERVICE_URL || "http://localhost:4106").replace(/\/$/, "");
|
|
43
46
|
const accessToken = ctx.accessToken;
|
|
@@ -94,6 +97,7 @@ export async function invokeTaskNative(
|
|
|
94
97
|
// A workflow task's own arguments, as the model wrote them against the task's schema.
|
|
95
98
|
...(ctx.args ?? {}),
|
|
96
99
|
},
|
|
100
|
+
...(ctx.trace?.executionId && ctx.trace?.nodeId ? { _meta: { [TRACE_META]: ctx.trace } } : {}),
|
|
97
101
|
},
|
|
98
102
|
undefined,
|
|
99
103
|
// A human is completing the wizard, or an Agent is working — lift the SDK's 60s default to 24h.
|
|
@@ -123,7 +123,7 @@ export function isWorkflowTask(mcp: DiscoveredMCP): boolean {
|
|
|
123
123
|
*/
|
|
124
124
|
export function workflowTaskInvoker(
|
|
125
125
|
mcp: DiscoveredMCP,
|
|
126
|
-
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken" | "baseUrl">,
|
|
126
|
+
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken" | "baseUrl" | "trace">,
|
|
127
127
|
): (input: unknown) => ReturnType<typeof invokeTaskNative> {
|
|
128
128
|
const toolId = mcp.taskTool as string;
|
|
129
129
|
return (input: unknown) => {
|
|
@@ -133,6 +133,7 @@ export function workflowTaskInvoker(
|
|
|
133
133
|
userId: identity.userId,
|
|
134
134
|
accessToken: identity.accessToken,
|
|
135
135
|
chatId: identity.chatId,
|
|
136
|
+
...(identity.trace ? { trace: { ...identity.trace, kind: "task" } } : {}),
|
|
136
137
|
...(identity.baseUrl ? { baseUrl: identity.baseUrl } : {}),
|
|
137
138
|
message: typeof message === "string" ? message : "",
|
|
138
139
|
...(Object.keys(rest).length ? { args: rest } : {}),
|
|
@@ -156,7 +157,7 @@ export function workflowTaskInvoker(
|
|
|
156
157
|
*/
|
|
157
158
|
export function componentAppInvoker(
|
|
158
159
|
mcp: DiscoveredMCP,
|
|
159
|
-
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken">,
|
|
160
|
+
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken" | "trace">,
|
|
160
161
|
): (input: unknown) => ReturnType<typeof invokeTaskNative> {
|
|
161
162
|
const toolId = mcp.component as string;
|
|
162
163
|
return (input: unknown) => {
|
|
@@ -166,6 +167,7 @@ export function componentAppInvoker(
|
|
|
166
167
|
userId: identity.userId,
|
|
167
168
|
accessToken: identity.accessToken,
|
|
168
169
|
chatId: identity.chatId,
|
|
170
|
+
...(identity.trace ? { trace: { ...identity.trace, kind: mcp.task ? "task" : "tool" } } : {}),
|
|
169
171
|
message: typeof message === "string" ? message : "",
|
|
170
172
|
...(Object.keys(hydrated).length ? { props: hydrated } : {}),
|
|
171
173
|
});
|
|
@@ -190,7 +192,7 @@ export function componentAppInvoker(
|
|
|
190
192
|
*/
|
|
191
193
|
export function workflowAppInvoker(
|
|
192
194
|
mcp: DiscoveredMCP,
|
|
193
|
-
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken" | "baseUrl">,
|
|
195
|
+
identity: Pick<AppInvocationContext, "conversationId" | "userId" | "chatId" | "accessToken" | "baseUrl" | "trace">,
|
|
194
196
|
): (input: unknown) => ReturnType<typeof invokeTaskNative> {
|
|
195
197
|
const toolId = String(mcp.methodName);
|
|
196
198
|
return (input: unknown) => {
|
|
@@ -200,6 +202,7 @@ export function workflowAppInvoker(
|
|
|
200
202
|
userId: identity.userId,
|
|
201
203
|
accessToken: identity.accessToken,
|
|
202
204
|
chatId: identity.chatId,
|
|
205
|
+
...(identity.trace ? { trace: { ...identity.trace, kind: mcp.task ? "task" : "tool" } } : {}),
|
|
203
206
|
...(identity.baseUrl ? { baseUrl: identity.baseUrl } : {}),
|
|
204
207
|
message: typeof message === "string" ? message : "",
|
|
205
208
|
...(Object.keys(hydrated).length ? { props: hydrated } : {}),
|
|
@@ -242,12 +245,18 @@ export function objectFunctionToolDef(fn: OfferedFunction): AgentToolDef {
|
|
|
242
245
|
* THAT row's object (a fund's `documents`, a document's `getDetail`), or answers 404 when the
|
|
243
246
|
* row's object offers no such function, which comes back to the model as the error it is.
|
|
244
247
|
*/
|
|
245
|
-
export function objectFunctionInvoker(
|
|
248
|
+
export function objectFunctionInvoker(
|
|
249
|
+
fn: OfferedFunction,
|
|
250
|
+
engineUrl: string,
|
|
251
|
+
/** Who is asking: the run and the calling node. The function records its own call under them. */
|
|
252
|
+
caller?: { executionId?: string; nodeId?: string },
|
|
253
|
+
): (input: unknown) => Promise<unknown> {
|
|
254
|
+
const who = caller?.executionId && caller?.nodeId ? `?${new URLSearchParams({ executionId: caller.executionId, caller: caller.nodeId })}` : "";
|
|
246
255
|
return async (input: unknown) => {
|
|
247
256
|
const { universal_id, ...params } = (input ?? {}) as Record<string, unknown>;
|
|
248
257
|
const id = typeof universal_id === "string" ? universal_id.trim() : "";
|
|
249
258
|
if (!id) return { error: `${fn.name} needs the universal_id of a result you already hold.` };
|
|
250
|
-
const res = await fetch(`${engineUrl}/dictionary/function/${encodeURIComponent(fn.name)}/${encodeURIComponent(id)}`, {
|
|
259
|
+
const res = await fetch(`${engineUrl}/dictionary/function/${encodeURIComponent(fn.name)}/${encodeURIComponent(id)}${who}`, {
|
|
251
260
|
method: "POST",
|
|
252
261
|
headers: { "content-type": "application/json" },
|
|
253
262
|
body: JSON.stringify(params),
|
|
@@ -105,6 +105,12 @@ export interface AgentToolDef {
|
|
|
105
105
|
|
|
106
106
|
/** Context a held app call needs to reach the human on the live session. */
|
|
107
107
|
export interface AppInvocationContext {
|
|
108
|
+
/**
|
|
109
|
+
* WHO IS CALLING, for the tool's own row on the trace (packages/docs/nodes/mcp-services.md,
|
|
110
|
+
* "Who records a call"): the run and the calling node, and whether the call opens a task.
|
|
111
|
+
* Sent as the call's `_meta`, never as an argument; absent for a call no model made.
|
|
112
|
+
*/
|
|
113
|
+
trace?: { executionId?: string; nodeId?: string; kind?: "tool" | "task" };
|
|
108
114
|
/** The caller's stream session — where the wizard renders and elicitation is answered. */
|
|
109
115
|
conversationId?: string;
|
|
110
116
|
userId?: string;
|