@unoverse-platform/studio 0.1.81 → 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/store.ts +51 -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
|
@@ -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
|
+
}
|