@unoverse-platform/studio 0.1.80 → 0.1.82
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/mcp.ts +3 -3
- package/local/plugin.ts +19 -3
- package/package.json +1 -1
- package/screens/TemplatesLibraryView.tsx +317 -0
- package/screens/TemplatesView.tsx +19 -5
- package/screens/index.ts +3 -0
- package/src/App.tsx +6 -1
- package/vendor/sdk/lib/app.tsx +181 -25
- package/vendor/sdk/lib/connection.tsx +11 -9
- package/vendor/sdk/lib/core/apps.ts +42 -5
- package/vendor/sdk/lib/core/client.ts +15 -12
- package/vendor/sdk/lib/core/connection.ts +7 -0
- package/vendor/sdk/lib/core/store.ts +76 -3
- package/vendor/sdk/lib/core/types.ts +20 -3
- package/vendor/sdk/lib/primitives.tsx +23 -0
- package/vendor/sdk/lib/render.tsx +5 -2
- package/vendor/sdk/lib/style.ts +4 -1
- package/vendor/sdk/main.tsx +26 -3
- package/vendor/sdk/responseView.tsx +82 -0
|
@@ -78,9 +78,20 @@ export interface UnoverseNode {
|
|
|
78
78
|
user?: UnoverseNode;
|
|
79
79
|
/** Timeline: data sub-tree rendered for an assistant turn (its ComponentSlot is scoped to that turn). */
|
|
80
80
|
assistant?: UnoverseNode;
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
|
|
81
|
+
/** TWO node types read this, and the shape says which.
|
|
82
|
+
*
|
|
83
|
+
* Each: the per-item subtree, rendered once per element of the bound array (`bind.items`),
|
|
84
|
+
* with that element as its data scope. The node's own `style` is the list container.
|
|
85
|
+
*
|
|
86
|
+
* Template: a STRING naming what the place holds (UNOVERSE_TEMPLATE_MODEL.md — holder
|
|
87
|
+
* grammar, deliberately the same shape as `interface:`), e.g. `template: grid-page`.
|
|
88
|
+
* A renderer branches on the node's `type`, never on this key alone. */
|
|
89
|
+
template?: UnoverseNode | string;
|
|
90
|
+
/** Template: the judgment granted to THIS placement — which items the director may select,
|
|
91
|
+
* whether it may order them, and the rules it works to. Opaque to the renderer, which
|
|
92
|
+
* carries it to the loader rather than reading into it; the placed node owns the mount,
|
|
93
|
+
* so the grant belongs on the placement and not on the template it holds. */
|
|
94
|
+
director?: Record<string, unknown>;
|
|
84
95
|
/** Literal content — components are microapps: static copy/lists/images are HARDCODED
|
|
85
96
|
* in the def; props exist only for state (UNOVERSE_AUTHORING.md). `bind` wins when set. */
|
|
86
97
|
value?: unknown; // Text
|
|
@@ -188,6 +199,10 @@ export interface UnoverseDefinition {
|
|
|
188
199
|
* active state's (resolveActiveView → the stateOrder ladder), falling back to
|
|
189
200
|
* `defaultLayout`. Absent → classic single-layout behavior via `root`. */
|
|
190
201
|
layouts?: Record<string, UnoverseNode>;
|
|
202
|
+
/** APP PLACES (UNOVERSE_TEMPLATE_MODEL §12): manifest `templates:` block plus any
|
|
203
|
+
* state entry that mounts a template (`template:`); the loader stamps the mounted
|
|
204
|
+
* template's own state names as `claims` — the mount state's reaction claim. */
|
|
205
|
+
templates?: Record<string, { template?: string; claims?: string[]; preview?: string[]; director?: Record<string, unknown> } & Record<string, unknown>>;
|
|
191
206
|
defaultLayout?: string;
|
|
192
207
|
/** THE DECLARED STATE ORDER (STATE_MODEL v2 §5 rules 3+5) — the template's priority
|
|
193
208
|
* ladder and arrival-scan order, served from the manifest. First entry outranks
|
|
@@ -251,6 +266,8 @@ export type StyleVariant = Condition & { apply: Record<string, unknown> };
|
|
|
251
266
|
// it does NOT bundle them. A brand change in design/styles is therefore refresh-only,
|
|
252
267
|
// exactly like a definition change (UNOVERSE_SPEC §2d-1).
|
|
253
268
|
export interface ResolvedTheme {
|
|
269
|
+
/** BREAKPOINTS (`breakpoint.*`) — named container-width bands responsive style maps key on. Served. */
|
|
270
|
+
breakpoint?: Record<string, string>;
|
|
254
271
|
color: Record<string, string>;
|
|
255
272
|
/** Webfont stylesheet URLs (served `fonts.stylesheets` data) — injected into
|
|
256
273
|
* document.head by the SDK (shadow roots can't register @font-face). */
|
|
@@ -118,6 +118,29 @@ function nodeChrome(node: UnoverseNode, data: Record<string, unknown>, theme: Re
|
|
|
118
118
|
styleEls.push(<style key="span-stack">{`@container (max-width:${w}){.${cls}{grid-column:1 / -1 !important}}`}</style>);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
// `columns` AS A BREAKPOINT MAP — the responsive grid: column counts per named band
|
|
122
|
+
// ({mobile: "1", tablet: "2", desktop: "3", full: "4"}), applied by CONTAINER width
|
|
123
|
+
// (the nearest `container` ancestor — a grid in a narrow panel stacks on a wide
|
|
124
|
+
// screen, which is what SDUI needs). `mobile` is the base band; a band not named
|
|
125
|
+
// inherits the previous one. Thresholds are SERVED (`theme.breakpoint.*`); the SDK
|
|
126
|
+
// holds no widths. The rule also marks the grid `container-type: inline-size`, so
|
|
127
|
+
// its own children (`span`) can query it, same as the inline `columns` form.
|
|
128
|
+
if (s?.columns && typeof s.columns === "object") {
|
|
129
|
+
const map = s.columns as Record<string, unknown>;
|
|
130
|
+
const base = map.mobile ?? map.tablet ?? map.desktop ?? map.full;
|
|
131
|
+
if (base != null) {
|
|
132
|
+
const cls = `uno-cols-${hashStr(JSON.stringify(map))}`;
|
|
133
|
+
let rules = `.${cls}{display:grid;grid-template-columns:repeat(${String(base)},minmax(0,1fr));align-content:start;container-type:inline-size}`;
|
|
134
|
+
for (const band of ["tablet", "desktop", "full"] as const) {
|
|
135
|
+
const w = theme.breakpoint?.[band];
|
|
136
|
+
if (map[band] != null && w) {
|
|
137
|
+
rules += `@container (min-width:${w}){.${cls}{grid-template-columns:repeat(${String(map[band])},minmax(0,1fr))}}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
classes.push(cls);
|
|
141
|
+
styleEls.push(<style key="columns">{rules}</style>);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
121
144
|
// `hideAbove` — the mirror: hide when the container is WIDER than the width. Together
|
|
122
145
|
// these are the whole layout-switch mechanism for space-responsive components: the
|
|
123
146
|
// compact layout carries hideAbove, the full layout hideBelow, and the container's
|
|
@@ -129,7 +129,10 @@ export function renderNode(
|
|
|
129
129
|
// nothing elastic remains for a resize to land on. Slot panels are injected at their
|
|
130
130
|
// frame (template.tsx); a layout root never carries appWidth (guard-enforced).
|
|
131
131
|
const panelW = (node as { appWidth?: string }).appWidth;
|
|
132
|
-
|
|
132
|
+
// ComponentSlot AND Template keep their appWidth: both are slot-dispatched places
|
|
133
|
+
// whose width applies to their FRAME (app.tsx), not to a box of their own —
|
|
134
|
+
// consuming it here left the mount frame width-less (measured live 2026-08-28).
|
|
135
|
+
if (panelW && node.type !== "ComponentSlot" && node.type !== "Template") {
|
|
133
136
|
const w = cssWidth(theme.appSize?.[panelW] ?? panelW);
|
|
134
137
|
if (w) node = { ...node, appWidth: undefined, style: { ...(node.style ?? {}), width: w, flex: "0 0 auto" } } as UnoverseNode;
|
|
135
138
|
}
|
|
@@ -146,7 +149,7 @@ export function renderNode(
|
|
|
146
149
|
if (node.type === "Orb") return <OrbView key={key} node={node} data={data} theme={theme} />;
|
|
147
150
|
if (node.type === "Input") return <InputView key={key} node={node} data={data} theme={theme} onAction={onAction} />;
|
|
148
151
|
if (node.type === "Markdown") return <MarkdownView key={key} node={node} data={data} theme={theme} />;
|
|
149
|
-
if (node.type === "ComponentSlot" || node.type === "Timeline" || node.type === "
|
|
152
|
+
if (node.type === "ComponentSlot" || node.type === "Timeline" || node.type === "Template") return slot ? slot(node, key) : null;
|
|
150
153
|
|
|
151
154
|
// `Ref` — REFERENCE, NOT COPY. The design system's shared pieces travel once and are
|
|
152
155
|
// cached; a definition names one and specialises it here (core/refs.ts: `props` remap,
|
package/vendor/sdk/lib/style.ts
CHANGED
|
@@ -121,7 +121,10 @@ function computeCss(s: Record<string, unknown>, theme: ResolvedTheme, data?: Rec
|
|
|
121
121
|
// correct for SDUI: the component reacts to its slot, not the page viewport). Value served.
|
|
122
122
|
if (s.container) (css as Record<string, unknown>).containerType = s.container;
|
|
123
123
|
// `columns` → an equal-width CSS grid (generic layout; the welcome card grid uses it).
|
|
124
|
-
|
|
124
|
+
// A MAP value ({mobile, tablet, ...}) is the RESPONSIVE form — handled in
|
|
125
|
+
// primitives.tsx as an injected rule, since inline styles cannot express the
|
|
126
|
+
// container queries it needs.
|
|
127
|
+
if (s.columns && typeof s.columns !== "object") {
|
|
125
128
|
css.display = "grid";
|
|
126
129
|
css.gridTemplateColumns = `repeat(${s.columns}, minmax(0, 1fr))`;
|
|
127
130
|
// THE GRID IS ITS OWN CONTAINER, so a child's `span` can collapse against the grid's
|
package/vendor/sdk/main.tsx
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createRoot, type Root } from "react-dom/client";
|
|
8
8
|
import { AppEngine, type AppConfig } from "./appEngine";
|
|
9
9
|
import { connectMcpAppHost } from "./mcpAppBridge";
|
|
10
|
+
import { ResponseView } from "./responseView";
|
|
10
11
|
|
|
11
12
|
type HostConfig = Omit<AppConfig, "getAccessToken"> & { token?: string };
|
|
12
13
|
|
|
@@ -82,13 +83,35 @@ connectMcpAppHost((r) => {
|
|
|
82
83
|
// hardcode/guess it — a baked URL ties the bundle to one environment and hides a
|
|
83
84
|
// missing config. If it's absent, fail loudly and don't mount.
|
|
84
85
|
const serverUrl = (meta["unoverse/serverUrl"] as string) || (out.serverUrl as string);
|
|
85
|
-
const appId = (out.app as string) || (meta["unoverse/app"] as string);
|
|
86
86
|
const conversationId = (out.conversationId as string) || (meta["unoverse/conversationId"] as string);
|
|
87
87
|
const token = (meta["unoverse/token"] as string) || undefined;
|
|
88
88
|
const userId = (meta["unoverse/userId"] as string) || undefined;
|
|
89
|
+
|
|
90
|
+
// THE NAMING RULE (UNOVERSE_MCP_APP_PROTOCOL § THE NAMING RULE): the RESULT names
|
|
91
|
+
// its interface. `interface.kind === "components"` → render exactly those (the
|
|
92
|
+
// response view — no app mount, no session, no home). `interface.kind === "app"`
|
|
93
|
+
// or a legacy `app`/`unoverse/app` boot fact (the app tools' results, internal
|
|
94
|
+
// hosts) → the named app mounts. Nothing mounts by default: a result naming
|
|
95
|
+
// nothing renders nothing, loudly.
|
|
96
|
+
const iface = out.interface as { kind?: string; components?: Record<string, unknown>[]; app?: string } | undefined;
|
|
97
|
+
if (iface?.kind === "components" && Array.isArray(iface.components)) {
|
|
98
|
+
if (!serverUrl) {
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.warn("[unoverse:boot] response result carried no serverUrl — cannot resolve definitions");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.log("[unoverse:boot] rendering RESPONSE from MCP host", { serverUrl, components: iface.components.length });
|
|
105
|
+
const el = document.getElementById("root")!;
|
|
106
|
+
if (!root) root = createRoot(el);
|
|
107
|
+
root.render(<ResponseView serverUrl={serverUrl} events={iface.components} />);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const appId = (iface?.kind === "app" ? iface.app : undefined) || (out.app as string) || (meta["unoverse/app"] as string);
|
|
89
112
|
if (!appId || !serverUrl) {
|
|
90
113
|
// eslint-disable-next-line no-console
|
|
91
|
-
console.warn("[unoverse:boot] cannot mount —
|
|
114
|
+
console.warn("[unoverse:boot] cannot mount — result named no interface (no components, no app)", {
|
|
92
115
|
serverUrl,
|
|
93
116
|
appId,
|
|
94
117
|
structuredContent: out,
|
|
@@ -97,6 +120,6 @@ connectMcpAppHost((r) => {
|
|
|
97
120
|
return;
|
|
98
121
|
}
|
|
99
122
|
// eslint-disable-next-line no-console
|
|
100
|
-
console.log("[unoverse:boot] mounting from MCP host", { serverUrl, appId, conversationId, userId, hasToken: Boolean(token) });
|
|
123
|
+
console.log("[unoverse:boot] mounting NAMED app from MCP host", { serverUrl, appId, conversationId, userId, hasToken: Boolean(token) });
|
|
101
124
|
apply({ serverUrl, appId, conversationId, userId, token });
|
|
102
125
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
}
|