@unoverse-platform/studio 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +46 -0
- package/package.json +5 -3
- package/screens/NativeAppHost.tsx +106 -0
- package/screens/TemplatesView.tsx +19 -4
- package/vite.config.ts +8 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unoverse
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @unoverse-platform/studio
|
|
2
|
+
|
|
3
|
+
Unoverse Studio: the standalone authoring app. It reads your own `rx/` project folder
|
|
4
|
+
off disk and previews your components, templates, skills and nodes. No database, no
|
|
5
|
+
Docker, no backend of its own.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @unoverse-platform/studio
|
|
9
|
+
npx unoverse-studio
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Studio opens on your project folder. Author in your editor, see it in Studio.
|
|
13
|
+
|
|
14
|
+
## What you author
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
rx/components/ components: definition-backed UI, rendered by the generic SDK
|
|
18
|
+
rx/templates/ templates: whole app surfaces, states and layouts
|
|
19
|
+
rx/styles/ themes and tokens
|
|
20
|
+
prompts/ skills and prompt blocks
|
|
21
|
+
nodes/ declarative YAML node manifests
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Everything is data. There is no codegen and no build step for your assets: the
|
|
25
|
+
definition IS the artifact, and the same definition renders on web, in ChatGPT, in
|
|
26
|
+
Claude, and on every other channel a universe serves.
|
|
27
|
+
|
|
28
|
+
## Connecting a universe
|
|
29
|
+
|
|
30
|
+
Studio on its own is a viewer for your folder. Connected to a universe it becomes a
|
|
31
|
+
full client:
|
|
32
|
+
|
|
33
|
+
- **Live preview**: Studio hosts the app natively. It asks the universe for its
|
|
34
|
+
MCP-served app shell (`ui://unoverse/app.html`, the same one ChatGPT loads) and the
|
|
35
|
+
app runs its own connections: chat, streaming, voice. Studio ships no renderer of
|
|
36
|
+
its own; the renderer loads when required, from the universe that owns it.
|
|
37
|
+
- **Publish**: your components, templates, skills and prompts publish into the
|
|
38
|
+
universe over its API, authorized by a publish key issued on that universe
|
|
39
|
+
(`unoverse key`). Lint runs first; nothing leaves your machine until the rules pass.
|
|
40
|
+
|
|
41
|
+
The universe is somebody's deployment of the Unoverse platform. Yours, your team's, or
|
|
42
|
+
a customer's. Studio never runs one.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unoverse-platform/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
|
|
6
6
|
"scripts": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"react-dom": "^18.2.0",
|
|
21
21
|
"react-oidc-context": "^3.1.0",
|
|
22
22
|
"tailwindcss": "^4.0.0",
|
|
23
|
-
"vite": "^6.0.0"
|
|
23
|
+
"vite": "^6.0.0",
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.25.1"
|
|
24
25
|
},
|
|
25
26
|
"license": "MIT",
|
|
26
27
|
"bin": {
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"public",
|
|
36
37
|
"index.html",
|
|
37
38
|
"vite.config.ts",
|
|
38
|
-
"README.md"
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
39
41
|
],
|
|
40
42
|
"publishConfig": {
|
|
41
43
|
"access": "public"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE NATIVE HOST (2026-07-28) — the whole point of MCP apps, applied to Studio itself.
|
|
3
|
+
*
|
|
4
|
+
* Live preview does NOT render with a compiled-in SDK. Studio is a bare MCP client:
|
|
5
|
+
* it reads `ui://unoverse/app.html` from the connected universe (the SAME single-file
|
|
6
|
+
* renderer ChatGPT and Claude load), puts it in an iframe, and posts it the standard
|
|
7
|
+
* `unoverse:config` message (web/sdk/src/main.tsx boot contract). From that moment the
|
|
8
|
+
* app drives its own MCP + /stream + voice connections against the universe — Studio
|
|
9
|
+
* hosts it exactly like any third-party surface would. The renderer loads when it is
|
|
10
|
+
* required, from the thing that owns it. Nothing is vendored, nothing is bundled.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useRef, useState } from "react";
|
|
13
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
14
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
15
|
+
|
|
16
|
+
const APP_UI_URI = "ui://unoverse/app.html";
|
|
17
|
+
|
|
18
|
+
export function NativeAppHost({
|
|
19
|
+
server,
|
|
20
|
+
token,
|
|
21
|
+
templateId,
|
|
22
|
+
userId,
|
|
23
|
+
conversationId,
|
|
24
|
+
}: {
|
|
25
|
+
/** Universe origin, e.g. http://localhost:4105 — the /mcp and /stream host. */
|
|
26
|
+
server: string;
|
|
27
|
+
token?: string;
|
|
28
|
+
templateId?: string;
|
|
29
|
+
userId: string;
|
|
30
|
+
conversationId: string;
|
|
31
|
+
}) {
|
|
32
|
+
const [html, setHtml] = useState<string | null>(null);
|
|
33
|
+
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
const frame = useRef<HTMLIFrameElement>(null);
|
|
35
|
+
|
|
36
|
+
// Pull the app shell from the universe — one resources/read, then the client closes.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
setHtml(null);
|
|
40
|
+
setError(null);
|
|
41
|
+
(async () => {
|
|
42
|
+
const authFetch = token
|
|
43
|
+
? async (url: string | URL, init?: RequestInit): Promise<Response> => {
|
|
44
|
+
const headers = new Headers(init?.headers);
|
|
45
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
46
|
+
return fetch(url, { ...init, headers });
|
|
47
|
+
}
|
|
48
|
+
: undefined;
|
|
49
|
+
const client = new Client({ name: "unoverse-studio-host", version: "1.0.0" });
|
|
50
|
+
try {
|
|
51
|
+
const transport = new StreamableHTTPClientTransport(
|
|
52
|
+
new URL(`${server.replace(/\/$/, "")}/mcp`),
|
|
53
|
+
authFetch ? { fetch: authFetch } : undefined,
|
|
54
|
+
);
|
|
55
|
+
await client.connect(transport);
|
|
56
|
+
const res = await client.readResource({ uri: APP_UI_URI });
|
|
57
|
+
const text = (res.contents?.[0] as { text?: string } | undefined)?.text;
|
|
58
|
+
if (!text) throw new Error(`${APP_UI_URI} returned no content — is the webSDK built on the universe?`);
|
|
59
|
+
if (!cancelled) setHtml(text);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
62
|
+
} finally {
|
|
63
|
+
client.close().catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return () => {
|
|
67
|
+
cancelled = true;
|
|
68
|
+
};
|
|
69
|
+
}, [server, token]);
|
|
70
|
+
|
|
71
|
+
// The boot contract: post `unoverse:config` on iframe load, and re-post whenever the
|
|
72
|
+
// config changes (also the token-refresh channel — main.tsx applies updates in place).
|
|
73
|
+
const postConfig = () => {
|
|
74
|
+
frame.current?.contentWindow?.postMessage(
|
|
75
|
+
{
|
|
76
|
+
type: "unoverse:config",
|
|
77
|
+
config: { serverUrl: server, apiUrl: server, templateId, userId, conversationId, token },
|
|
78
|
+
},
|
|
79
|
+
"*",
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
+
useEffect(postConfig, [token, templateId, userId, conversationId, server, html]);
|
|
84
|
+
|
|
85
|
+
if (error)
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex h-full items-center justify-center p-8 text-center text-sm text-red-500">
|
|
88
|
+
<div>
|
|
89
|
+
<div className="font-semibold">The universe did not serve the app</div>
|
|
90
|
+
<div className="mt-1 opacity-80">{error}</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
if (!html)
|
|
95
|
+
return <div className="flex h-full items-center justify-center text-sm opacity-60">Loading the app from the universe…</div>;
|
|
96
|
+
return (
|
|
97
|
+
<iframe
|
|
98
|
+
ref={frame}
|
|
99
|
+
srcDoc={html}
|
|
100
|
+
onLoad={postConfig}
|
|
101
|
+
title="unoverse app"
|
|
102
|
+
style={{ width: "100%", height: "100%", border: 0, display: "block" }}
|
|
103
|
+
allow="microphone; autoplay"
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -12,6 +12,7 @@ import { useAuth } from "react-oidc-context";
|
|
|
12
12
|
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
13
13
|
import { ComponentStore } from "@unoverse/sdk";
|
|
14
14
|
import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resolveActiveLayout, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
|
|
15
|
+
import { NativeAppHost } from "./NativeAppHost";
|
|
15
16
|
import { client, authedFetch, hasAuth, UNOVERSE_BASE } from "studio-host";
|
|
16
17
|
|
|
17
18
|
// Per-APP test conversation: each app chats in its own conversation id, so testing
|
|
@@ -278,10 +279,12 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
|
|
|
278
279
|
[binding?.workflow, binding?.trigger, tokenSub],
|
|
279
280
|
);
|
|
280
281
|
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
282
|
+
// NATIVE (2026-07-28): Live mode is the NATIVE HOST — an iframe running the
|
|
283
|
+
// universe-served ui:// app (NativeAppHost), which opens its OWN connections. The
|
|
284
|
+
// in-page data-plane connection below is therefore never opened; it remains only
|
|
285
|
+
// because Mock rendering shares this machinery (store, states, width).
|
|
286
|
+
const liveIsNative = live && Boolean(binding?.workflow) && (!hasAuth || Boolean(auth?.isAuthenticated));
|
|
287
|
+
const canConnect = false;
|
|
285
288
|
const rawConn = useUnoverseConnection(config, session, store, { enabled: canConnect });
|
|
286
289
|
// HARD INVARIANT (workbench doc Mode A — "Mock does not connect with server"): a Mock
|
|
287
290
|
// session must NEVER reach the backend. The SDK only gates the inbound SSE stream on
|
|
@@ -874,6 +877,17 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
|
|
|
874
877
|
...((selectedDef?.fluidHeight ?? true) ? { height: "100%" } : {}),
|
|
875
878
|
}}
|
|
876
879
|
>
|
|
880
|
+
{liveIsNative ? (
|
|
881
|
+
/* NATIVE: the universe serves the app; Studio just hosts it. Same
|
|
882
|
+
ui:// shell ChatGPT loads, same boot contract, its own connections. */
|
|
883
|
+
<NativeAppHost
|
|
884
|
+
server={UNOVERSE_BASE || `http://${connCfg.server}`}
|
|
885
|
+
token={token}
|
|
886
|
+
templateId={`${selectedDef?.org ? `${selectedDef.org}/` : ""}${selected ?? ""}`}
|
|
887
|
+
userId={session.userId}
|
|
888
|
+
conversationId={session.conversationId}
|
|
889
|
+
/>
|
|
890
|
+
) : (
|
|
877
891
|
<StreamedUnoverseTemplate
|
|
878
892
|
client={client}
|
|
879
893
|
store={store}
|
|
@@ -968,6 +982,7 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
|
|
|
968
982
|
setVersion((v) => v + 1);
|
|
969
983
|
}}
|
|
970
984
|
/>
|
|
985
|
+
)}
|
|
971
986
|
</div>
|
|
972
987
|
</div>
|
|
973
988
|
)}
|
package/vite.config.ts
CHANGED
|
@@ -39,14 +39,14 @@ export default defineConfig({
|
|
|
39
39
|
"@unoverse/studio": resolve(__dirname, "screens/index.ts"),
|
|
40
40
|
// The host seam. Canvas aliases this to a universe; here it is the local disk.
|
|
41
41
|
"studio-host": resolve(__dirname, "src/host.ts"),
|
|
42
|
-
// The renderer
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
"@unoverse/sdk": existsSync(resolve(__dirname, "
|
|
48
|
-
? resolve(__dirname, "
|
|
49
|
-
: resolve(__dirname, "
|
|
42
|
+
// The renderer. THE REAL SOURCE WHEN IT EXISTS (the monorepo), the vendored copy
|
|
43
|
+
// otherwise (an installed package, where prepack wrote vendor/sdk and the monorepo
|
|
44
|
+
// path resolves to nothing). The old order preferred vendor — so the moment a
|
|
45
|
+
// publish ran vendor-sdk.mjs, the monorepo's own Studio silently switched onto a
|
|
46
|
+
// FROZEN SDK copy and live SDK edits stopped appearing (2026-07-28).
|
|
47
|
+
"@unoverse/sdk": existsSync(resolve(__dirname, "../unoverse/web/sdk/src/lib/index.ts"))
|
|
48
|
+
? resolve(__dirname, "../unoverse/web/sdk/src/lib/index.ts")
|
|
49
|
+
: resolve(__dirname, "vendor/sdk/lib/index.ts"),
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
ssr: {
|