@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
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* <UnoverseComponent> — reads a definition by URI from the Unoverse MCP server
|
|
3
|
-
* (via ./core) and renders it natively.
|
|
4
|
-
*
|
|
5
|
-
* ONE COMPONENT, TWO DATA SOURCES. A definition cannot tell which is rendering it, so the
|
|
6
|
-
* SDK stopped pretending they were different things:
|
|
7
|
-
*
|
|
8
|
-
* data source give it state lives in
|
|
9
|
-
* a plain object `uri` + `data` a local slice, here
|
|
10
|
-
* the live stream `store` + `chatId` + `nodeId` the store's slice
|
|
11
|
-
*
|
|
12
|
-
* The split used to be two exported components, and a host had to know which one it was
|
|
13
|
-
* holding. It never mattered to the component: the same `renderNode` walks the same
|
|
14
|
-
* definition, and `setValue` writes the same instance either way.
|
|
15
|
-
*/
|
|
16
|
-
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore, type CSSProperties, type ReactNode } from "react";
|
|
17
|
-
import { dispatchAction, matches, type ComponentStore, type ResolvedTheme, type UnoverseClient, type UnoverseDefinition } from "./core";
|
|
18
|
-
import { renderNode, keyframesCss, type ActionHandler, type SlotResolver } from "./render";
|
|
19
|
-
import { useUnoverseTheme, ensureFontStylesheets } from "./theme";
|
|
20
|
-
import { IsolatedRoot } from "./isolate";
|
|
21
|
-
import { stateOfUri } from "./core/apps";
|
|
22
|
-
|
|
23
|
-
/** Subscribe to an instance in the store, re-rendering when a merge lands. Snapshots THIS
|
|
24
|
-
* instance's slice (merge() gives it a fresh identity), so chunks for other components bail
|
|
25
|
-
* out in React instead of re-rendering everyone. Tolerates no store: the static path calls
|
|
26
|
-
* the same hook, because hooks cannot be conditional. */
|
|
27
|
-
export function useUnoverseInstance(store?: ComponentStore, chatId = "", nodeId = "") {
|
|
28
|
-
const subscribe = useCallback((cb: () => void) => (store ? store.subscribe(cb) : () => {}), [store]);
|
|
29
|
-
const data = useSyncExternalStore(
|
|
30
|
-
subscribe,
|
|
31
|
-
useCallback(() => (store ? store.get(chatId, nodeId) : undefined), [store, chatId, nodeId]),
|
|
32
|
-
);
|
|
33
|
-
const type = useSyncExternalStore(
|
|
34
|
-
subscribe,
|
|
35
|
-
useCallback(() => (store ? store.getType(chatId, nodeId) : undefined), [store, chatId, nodeId]),
|
|
36
|
-
);
|
|
37
|
-
const componentUrl = useSyncExternalStore(
|
|
38
|
-
subscribe,
|
|
39
|
-
useCallback(() => (store ? store.getComponentUrl(chatId, nodeId) : undefined), [store, chatId, nodeId]),
|
|
40
|
-
);
|
|
41
|
-
return { type, data, componentUrl };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface UnoverseComponentProps {
|
|
45
|
-
client: UnoverseClient;
|
|
46
|
-
/** STATIC source: the definition to render, e.g. "unoverse://components/card". */
|
|
47
|
-
uri?: string;
|
|
48
|
-
/** STATIC source: instance data bound into the definition. */
|
|
49
|
-
data?: Record<string, unknown>;
|
|
50
|
-
/** LIVE source: the merge-state store. With it, `uri` and `data` come from the instance. */
|
|
51
|
-
store?: ComponentStore;
|
|
52
|
-
chatId?: string;
|
|
53
|
-
nodeId?: string;
|
|
54
|
-
/** Neutral turn state merged into the data scope (e.g. `streaming`), so a component can
|
|
55
|
-
* reflect it. Live source only. */
|
|
56
|
-
extraData?: Record<string, unknown>;
|
|
57
|
-
onAction?: ActionHandler;
|
|
58
|
-
/**
|
|
59
|
-
* Resolved theme. Optional — if omitted, the SDK FETCHES it from the server
|
|
60
|
-
* (`unoverse://theme/light`). The SDK bundles no tokens. Pass one to override
|
|
61
|
-
* (e.g. the workbench's light/dark toggle); swap it to restyle, zero def changes.
|
|
62
|
-
*/
|
|
63
|
-
theme?: ResolvedTheme;
|
|
64
|
-
/**
|
|
65
|
-
* Render inside a Shadow DOM for full CSS isolation (default true). The SDK owns the
|
|
66
|
-
* isolation boundary so every consumer is protected identically; the canvas/channel no
|
|
67
|
-
* longer wrap it themselves. Set false only for a host that manages its own isolation.
|
|
68
|
-
*/
|
|
69
|
-
isolate?: boolean;
|
|
70
|
-
/**
|
|
71
|
-
* A SURFACE'S SINGLE OCCUPANT FILLS THE SURFACE (docs/design/05): set by a limit-1
|
|
72
|
-
* reaction surface so the instance wrapper takes the frame's full height. The
|
|
73
|
-
* component still owns its face — this only gives its `height: "full"` a definite
|
|
74
|
-
* container. Flow/rail instances stay content-sized (never set there).
|
|
75
|
-
*/
|
|
76
|
-
fill?: boolean;
|
|
77
|
-
/**
|
|
78
|
-
* WHAT A `Place` IN THIS DEFINITION DRAWS. An app's places are filled by the screen
|
|
79
|
-
* (app.tsx); a place inside a component or template is filled by its host, which alone
|
|
80
|
-
* knows what belongs there (a fullscreen template's place holds the page's avatar).
|
|
81
|
-
* Without it a place draws nothing.
|
|
82
|
-
*/
|
|
83
|
-
place?: (name: string) => ReactNode;
|
|
84
|
-
/**
|
|
85
|
-
* THE APP'S STATE, when it lives in another store (live source only). A template a website
|
|
86
|
-
* page draws keeps its own slice in its own store, and shares one app state with the rest
|
|
87
|
-
* of the page: it reads that state beneath its own values, and `setAppValue` writes there.
|
|
88
|
-
*/
|
|
89
|
-
appStore?: ComponentStore;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function UnoverseComponent({ client, uri, data, store, chatId, nodeId, extraData, onAction, theme, isolate = true, fill = false, place, appStore }: UnoverseComponentProps) {
|
|
93
|
-
const [def, setDef] = useState<UnoverseDefinition | null>(null);
|
|
94
|
-
const [error, setError] = useState<string | null>(null);
|
|
95
|
-
// A component's OWN view state (a tab, a step, an expanded face) moves by writing its own
|
|
96
|
-
// slice. That write is not a host concern: a tab click should work wherever a component
|
|
97
|
-
// renders, without every surface wiring a handler for it. So the renderer keeps the slice
|
|
98
|
-
// and applies `setValue` to it, and STILL forwards every action to `onAction` for hosts
|
|
99
|
-
// that care (a channel persisting state, a workflow answering a submit).
|
|
100
|
-
const [localSlice, setLocalSlice] = useState<Record<string, unknown>>({});
|
|
101
|
-
// LIVE SOURCE. The hook runs either way (hooks cannot be conditional) and no-ops without a
|
|
102
|
-
// store, so one component serves both paths without a wrapper deciding for it.
|
|
103
|
-
const live = useUnoverseInstance(store, chatId, nodeId);
|
|
104
|
-
const appState = useSyncExternalStore(
|
|
105
|
-
useCallback((cb: () => void) => (appStore ? appStore.subscribe(cb) : () => {}), [appStore]),
|
|
106
|
-
useCallback(() => appStore?.getAppState(), [appStore]),
|
|
107
|
-
);
|
|
108
|
-
// The SERVER-SENT address first: an org component's URI is org-qualified
|
|
109
|
-
// (`unoverse://components/<org>/<name>`) and the bare type cannot rebuild it once two
|
|
110
|
-
// orgs share a name. The rebuild-from-type stays as the fallback for events that
|
|
111
|
-
// carry no componentUrl. (Still the plain URI, never the withdrawn `@refs` form —
|
|
112
|
-
// that suffix broke Studio's local definition route.)
|
|
113
|
-
// THE HASH NAMES THE STATE (stateOfUri): the address is read without it, and it seeds the
|
|
114
|
-
// public `state` of a static instance that carries none, so `...#page` opens on the page.
|
|
115
|
-
const address = store ? (live.componentUrl ?? (live.type ? `unoverse://components/${live.type}` : null)) : (uri ?? null);
|
|
116
|
-
const { uri: resolvedUri, state: hashState } = address ? stateOfUri(address) : { uri: null as string | null };
|
|
117
|
-
const sourceData = store
|
|
118
|
-
? (extraData ? { ...(live.data ?? {}), ...extraData } : live.data)
|
|
119
|
-
: (hashState && data?.state == null ? { state: hashState, ...(data ?? {}) } : data);
|
|
120
|
-
// No theme passed → fetch it from the server (the SDK owns zero token values). The
|
|
121
|
-
// theme REF comes from the definition itself: a template's `org` names its client's
|
|
122
|
-
// theme (`<org>/light`); a universal component previews under the default set
|
|
123
|
-
// ("light" — channels pass their org's theme explicitly). Wait for the def before
|
|
124
|
-
// fetching (null = no fetch) so the wrong theme never flashes.
|
|
125
|
-
const fetched = useUnoverseTheme(client, theme ? null : def ? (def.org ? `${def.org}/light` : "light") : null);
|
|
126
|
-
const activeTheme = theme ?? fetched;
|
|
127
|
-
// Register the theme's webfonts (served DATA) at document level — see ensureFontStylesheets.
|
|
128
|
-
useEffect(() => ensureFontStylesheets(activeTheme), [activeTheme]);
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
let alive = true;
|
|
132
|
-
setDef(null);
|
|
133
|
-
setError(null);
|
|
134
|
-
setLocalSlice({});
|
|
135
|
-
if (!resolvedUri) return;
|
|
136
|
-
client
|
|
137
|
-
.readDefinition(resolvedUri)
|
|
138
|
-
.then((d) => alive && setDef(d))
|
|
139
|
-
.catch((e) => alive && setError(String(e?.message ?? e)));
|
|
140
|
-
return () => {
|
|
141
|
-
alive = false;
|
|
142
|
-
};
|
|
143
|
-
}, [client, resolvedUri]);
|
|
144
|
-
|
|
145
|
-
// Fill props the instance didn't provide from the definition's defaults; instance `data`
|
|
146
|
-
// overrides. This is what lets a partially-emitted component still render its declared
|
|
147
|
-
// baseline — e.g. StreamingText's `progressText` default ("Thinking…") shows while the node
|
|
148
|
-
// streams and hasn't emitted a progress line. IMPORTANT: a def `default` must therefore be
|
|
149
|
-
// a sensible RUNTIME fallback, NOT a demo placeholder — a demo-y default (e.g. a markdown
|
|
150
|
-
// showcase on `text`) would mask an empty stream. Keep demo content out of prop defaults.
|
|
151
|
-
// Memoized on the def — a streaming component re-renders per chunk; the defaults don't move.
|
|
152
|
-
const propDefaults = useMemo(
|
|
153
|
-
() =>
|
|
154
|
-
Object.fromEntries(
|
|
155
|
-
Object.entries((def?.props ?? {}) as Record<string, { default?: unknown }>).map(([k, v]) => [k, v?.default]),
|
|
156
|
-
),
|
|
157
|
-
[def],
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// ONE ACTION RULE, TWO OWNERS OF THE SLICE. `setValue` writes THIS instance's keys; where
|
|
161
|
-
// they land is the only difference between the two sources, and the component never knows.
|
|
162
|
-
const handleAction = useMemo<ActionHandler>(
|
|
163
|
-
() =>
|
|
164
|
-
store
|
|
165
|
-
? (action, scope) =>
|
|
166
|
-
dispatchAction(action, scope, {
|
|
167
|
-
store,
|
|
168
|
-
chatId: chatId ?? "",
|
|
169
|
-
nodeId: nodeId ?? "",
|
|
170
|
-
appStore,
|
|
171
|
-
// Server actions carry the INSTANCE context (which component + its current
|
|
172
|
-
// slice) so the channel can forward the submitted state with the action.
|
|
173
|
-
sendToServer: onAction
|
|
174
|
-
? (t, payload) =>
|
|
175
|
-
onAction(t, payload, {
|
|
176
|
-
chatId,
|
|
177
|
-
nodeId,
|
|
178
|
-
type: live.type,
|
|
179
|
-
state: store.get(chatId ?? "", nodeId ?? ""),
|
|
180
|
-
})
|
|
181
|
-
: undefined,
|
|
182
|
-
})
|
|
183
|
-
: (action, payload, meta) => {
|
|
184
|
-
// No store, so the slice is local. This is what makes a tab work in a preview,
|
|
185
|
-
// a content panel or an email-shaped host with no state code in between.
|
|
186
|
-
if (action && typeof action === "object" && (action as { type?: string }).type === "setValue") {
|
|
187
|
-
const values = ((action as { values?: { key: string; value: unknown }[] }).values ?? []).filter(
|
|
188
|
-
(v) => v && typeof v.key === "string",
|
|
189
|
-
);
|
|
190
|
-
if (values.length)
|
|
191
|
-
setLocalSlice((prev) => ({ ...prev, ...Object.fromEntries(values.map((v) => [v.key, v.value])) }));
|
|
192
|
-
}
|
|
193
|
-
onAction?.(action, payload, meta);
|
|
194
|
-
},
|
|
195
|
-
[store, chatId, nodeId, onAction, live.type, appStore],
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
// ARROW KEYS STEP A TEMPLATE. A template's steps (the served stateTree: `on` + its
|
|
199
|
-
// substates) move with Left and Right while it holds the main place, through the same
|
|
200
|
-
// setValue its own Back and Next make, so the step is saved and reported exactly as a
|
|
201
|
-
// click would be. Forms (components) are left alone: their steps are confirmations.
|
|
202
|
-
// Typing in a field never steps.
|
|
203
|
-
const slice = store ? sourceData : localSlice;
|
|
204
|
-
useEffect(() => {
|
|
205
|
-
const d = def as (UnoverseDefinition & {
|
|
206
|
-
kind?: string;
|
|
207
|
-
stateTree?: { name: string; on?: string; states?: { name: string }[] }[];
|
|
208
|
-
statePlaces?: Record<string, string>;
|
|
209
|
-
}) | null;
|
|
210
|
-
if (d?.kind !== "template") return;
|
|
211
|
-
const face = String((slice as Record<string, unknown> | undefined)?.state ?? d.state?.state ?? "");
|
|
212
|
-
const tree = (d.stateTree ?? []).find((s) => s.name === face);
|
|
213
|
-
const steps = tree?.states?.map((s) => s.name) ?? [];
|
|
214
|
-
if (!tree?.on || steps.length < 2 || d.statePlaces?.[face] !== "main") return;
|
|
215
|
-
const on = tree.on;
|
|
216
|
-
const onKey = (e: KeyboardEvent) => {
|
|
217
|
-
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
|
|
218
|
-
const t = e.target as HTMLElement | null;
|
|
219
|
-
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
|
|
220
|
-
const cur = String((slice as Record<string, unknown> | undefined)?.[on] ?? d.state?.[on] ?? 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
|
-
handleAction({ type: "setValue", values: [{ key: on, value: steps[i] }] }, {});
|
|
225
|
-
};
|
|
226
|
-
document.addEventListener("keydown", onKey);
|
|
227
|
-
return () => document.removeEventListener("keydown", onKey);
|
|
228
|
-
}, [def, slice, handleAction]);
|
|
229
|
-
|
|
230
|
-
// Loading/error hints carry NO styling — the SDK owns zero styles (tokens or hints).
|
|
231
|
-
if (error) return <div>Unoverse error: {error}</div>;
|
|
232
|
-
if (store && !live.type) return <div>waiting for COMPONENT_INIT…</div>;
|
|
233
|
-
if (!def || !activeTheme) return <div>Loading {resolvedUri ?? "component"}…</div>;
|
|
234
|
-
// Internal state initial values (def.state) sit beneath the live slice — the
|
|
235
|
-
// microapp's own keys (step/phase/…) start at their authored values and move only
|
|
236
|
-
// when its actions write them. Props stay the EXTERNAL contract.
|
|
237
|
-
// The local slice sits ON TOP: a value the component wrote itself is the current one.
|
|
238
|
-
const merged = store
|
|
239
|
-
? { ...propDefaults, ...(appState ?? {}), ...(def?.state ?? {}), ...(sourceData ?? {}) }
|
|
240
|
-
: { ...propDefaults, ...(def?.state ?? {}), ...(sourceData ?? {}), ...localSlice };
|
|
241
|
-
// Inject the served keyframes + apply the SERVED base render-root (theme.root — DS typography
|
|
242
|
-
// + font-smoothing). When ISOLATED, theme.root is applied to the shadow-root container inside
|
|
243
|
-
// IsolatedRoot (a REAL box — reliable font cascade in the shadow tree). When NOT isolated the
|
|
244
|
-
// host owns isolation, so apply it here on a `display:contents` wrapper (out of layout while
|
|
245
|
-
// its inherited props cascade) — the design-system baseline either way.
|
|
246
|
-
const keyframes = <style>{keyframesCss(activeTheme)}</style>;
|
|
247
|
-
// The shared pieces this definition NAMES travel beside it and are threaded through
|
|
248
|
-
// every recursion, so a reference nested in a Switch branch or an Each item resolves too.
|
|
249
|
-
// A DEFINITION THAT CARRIES SLOTS RESOLVES THEM — the rule the SDK already applies to
|
|
250
|
-
// an app tree, applied here too. Without it a rendered TEMPLATE drew its frame and every
|
|
251
|
-
// section resolved to null (render.tsx: `slot ? slot(node) : null`), so a page that
|
|
252
|
-
// STREAMED IN rather than being mounted by an app came out blank. Mounted templates were
|
|
253
|
-
// fine because `app.tsx` hands that path a resolver; instances never got one, and a
|
|
254
|
-
// definition rendering its own parts nowhere is a renderer bug, not a design decision.
|
|
255
|
-
//
|
|
256
|
-
// Generic and dumb, like the rest of it: the slot's claim picks instances out of the
|
|
257
|
-
// SAME store by the face they wear, in the order the store holds them. It knows nothing
|
|
258
|
-
// about templates, days or cards — only "which instances match this claim".
|
|
259
|
-
//
|
|
260
|
-
// TEMPLATES ONLY, and the boundary matters: an ordinary component may carry the
|
|
261
|
-
// conversation's FLOW slot (`select: {}` inside a chat's assistant turn), and that one
|
|
262
|
-
// belongs to the app's timeline, which scopes it to a single turn's unclaimed
|
|
263
|
-
// components. Resolving it here too rendered every instance in the store a second time
|
|
264
|
-
// — the composed page appeared in its surface AND in the chat beside it.
|
|
265
|
-
const resolveSlot: SlotResolver | undefined = (store && (def as { sections?: unknown }).sections) || place
|
|
266
|
-
? (node, key) => {
|
|
267
|
-
if (node.type === "Place") return place?.(String((node as { name?: unknown }).name ?? "")) ?? null;
|
|
268
|
-
if (!store) return null;
|
|
269
|
-
const claim = node.select?.where;
|
|
270
|
-
const pointers = store
|
|
271
|
-
.getResponses()
|
|
272
|
-
.flatMap((r) => r.components)
|
|
273
|
-
.filter((p) => {
|
|
274
|
-
const { chatId: c, nodeId: n } = store.split(p);
|
|
275
|
-
if (n === nodeId) return false; // never claim the instance drawing the slot
|
|
276
|
-
return claim ? matches(claim, (store.get(c, n) ?? {}) as Record<string, unknown>) : true;
|
|
277
|
-
})
|
|
278
|
-
.slice(0, node.select?.limit ?? undefined);
|
|
279
|
-
if (!pointers.length)
|
|
280
|
-
return node.fallback ? renderNode(node.fallback, {}, handleAction, activeTheme, key ?? "fb", undefined, def.defs) : null;
|
|
281
|
-
return pointers.map((p) => {
|
|
282
|
-
const { chatId: c, nodeId: n } = store.split(p);
|
|
283
|
-
return (
|
|
284
|
-
<UnoverseComponent
|
|
285
|
-
key={n}
|
|
286
|
-
client={client}
|
|
287
|
-
store={store}
|
|
288
|
-
chatId={c}
|
|
289
|
-
nodeId={n}
|
|
290
|
-
onAction={onAction}
|
|
291
|
-
theme={activeTheme}
|
|
292
|
-
appStore={appStore}
|
|
293
|
-
/>
|
|
294
|
-
);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
: undefined;
|
|
298
|
-
const tree = renderNode(def.root, merged, handleAction, activeTheme, undefined, resolveSlot, def.defs);
|
|
299
|
-
// The isolation container MIRRORS the component root's declared sizing intent (§4a:
|
|
300
|
-
// the component owns its size; the host responds). A root that declares
|
|
301
|
-
// `height: "full"` needs a definite chain — the container passes the host's height
|
|
302
|
-
// through (100%). Any other root keeps an auto container → fit-to-content, the §5a
|
|
303
|
-
// default (a card in a timeline must NOT fill). Without this, a full-height
|
|
304
|
-
// component's 100% resolves against an auto box and silently collapses.
|
|
305
|
-
const fillsHost = fill || ((def.root as { style?: Record<string, unknown> })?.style?.height ?? null) === "full";
|
|
306
|
-
const isolatedRootStyle = {
|
|
307
|
-
...(activeTheme.root as CSSProperties),
|
|
308
|
-
...(fillsHost ? { height: "100%" } : {}),
|
|
309
|
-
...(fill ? { flex: "1 1 auto", minHeight: 0 } : {}),
|
|
310
|
-
};
|
|
311
|
-
if (isolate) {
|
|
312
|
-
return (
|
|
313
|
-
<IsolatedRoot rootStyle={isolatedRootStyle}>
|
|
314
|
-
{keyframes}
|
|
315
|
-
{tree}
|
|
316
|
-
</IsolatedRoot>
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
return (
|
|
320
|
-
<>
|
|
321
|
-
{keyframes}
|
|
322
|
-
<div style={{ display: "contents", ...(activeTheme.root as CSSProperties) }}>{tree}</div>
|
|
323
|
-
</>
|
|
324
|
-
);
|
|
325
|
-
}
|