@unoverse-platform/studio 0.1.26 → 0.1.28

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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/screens/AtomsView.tsx +40 -2
  3. package/screens/ComponentsView.tsx +112 -21
  4. package/screens/NativeAppHost.tsx +8 -4
  5. package/screens/TemplatesView.tsx +51 -21
  6. package/screens/states.ts +27 -4
  7. package/vendor/sdk/appEngine.tsx +20 -4
  8. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -2
  9. package/vendor/sdk/lib/core/actions.ts +10 -6
  10. package/vendor/sdk/lib/core/client.ts +39 -12
  11. package/vendor/sdk/lib/core/index.ts +21 -0
  12. package/vendor/sdk/lib/core/refs.ts +117 -0
  13. package/vendor/sdk/lib/core/store.ts +65 -38
  14. package/vendor/sdk/lib/core/templates.ts +270 -0
  15. package/vendor/sdk/lib/core/types.ts +27 -7
  16. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +131 -0
  17. package/vendor/sdk/lib/orb/engine/braid.ts +50 -0
  18. package/vendor/sdk/lib/orb/engine/core.ts +167 -0
  19. package/vendor/sdk/lib/orb/engine/lattice.ts +205 -0
  20. package/vendor/sdk/lib/orb/engine/morph.ts +138 -0
  21. package/vendor/sdk/lib/orb/engine/orbits.ts +80 -0
  22. package/vendor/sdk/lib/orb/engine/profiles.ts +162 -0
  23. package/vendor/sdk/lib/orb/engine/registry.ts +31 -0
  24. package/vendor/sdk/lib/orb/engine/ribbon.ts +93 -0
  25. package/vendor/sdk/lib/orb/engine/rings.ts +239 -0
  26. package/vendor/sdk/lib/orb/engine/types.ts +14 -0
  27. package/vendor/sdk/lib/orb/engine/web.ts +98 -0
  28. package/vendor/sdk/lib/orb/index.ts +20 -0
  29. package/vendor/sdk/lib/orb/presets.ts +181 -0
  30. package/vendor/sdk/lib/orb/theme.ts +83 -0
  31. package/vendor/sdk/lib/orb/types.ts +84 -0
  32. package/vendor/sdk/lib/primitives.tsx +116 -2
  33. package/vendor/sdk/lib/render.tsx +32 -8
  34. package/vendor/sdk/lib/style.ts +6 -2
  35. package/vendor/sdk/lib/template.tsx +86 -224
  36. package/vendor/skills/unoverse-create/SKILL.md +18 -18
  37. package/vendor/skills/unoverse-create/references/agent-skill.md +12 -12
  38. package/vendor/skills/unoverse-create/references/block.md +5 -5
  39. package/vendor/skills/unoverse-create/references/component.md +150 -75
  40. package/vendor/skills/unoverse-create/references/node.md +1 -1
  41. package/vendor/skills/unoverse-create/references/template.md +142 -82
  42. package/vendor/skills/unoverse-create/references/workflow.md +15 -15
@@ -13,8 +13,30 @@
13
13
  * chat primitives (Conversation/Input/…) come in later slices.
14
14
  */
15
15
  import { useCallback, useEffect, useMemo, useState, useSyncExternalStore, type CSSProperties, type ReactNode } from "react";
16
- import { dispatchAction, type ComponentStore, type ResolvedTheme, type UnoverseClient, type UnoverseDefinition, type UnoverseNode } from "./core";
17
- import { renderNode, styleToCss, keyframesCss, cssWidth, type ActionHandler, type SlotResolver } from "./render";
16
+ import {
17
+ collectSurfacedViews,
18
+ cssWidth,
19
+ dispatchAction,
20
+ formatRelative,
21
+ isViewField,
22
+ orderedClaims,
23
+ propDefaults,
24
+ resolveActiveLayout,
25
+ resolveActiveView,
26
+ selectPointers,
27
+ viewOf,
28
+ type ComponentStore,
29
+ type ResolvedTheme,
30
+ type UnoverseClient,
31
+ type UnoverseDefinition,
32
+ type UnoverseNode,
33
+ } from "./core";
34
+ import { renderNode, styleToCss, keyframesCss, type ActionHandler, type SlotResolver } from "./render";
35
+
36
+ // The pure template derivations moved to core/templates.ts (the portable brain —
37
+ // framework-agnostic, guarded by sdk-core-portable.test.ts). Re-exported here so
38
+ // existing consumers keep their import path.
39
+ export { collectSurfacedViews, computeAppWidth, orderedClaims, resolveActiveLayout, resolveActiveView, selectPointers, viewOf } from "./core";
18
40
  import { UnoverseComponent } from "./UnoverseComponent";
19
41
  import { useUnoverseTheme, ensureFontStylesheets } from "./theme";
20
42
  import { IsolatedRoot } from "./isolate";
@@ -49,203 +71,6 @@ const INERT_VOICE: UseVoiceServiceConfig = {
49
71
  session: { userId: "", conversationId: "", workflowId: "", targetTriggerNode: "" },
50
72
  };
51
73
 
52
- /** The author's declared prop defaults (def.props[k].default) → a flat data object. Content, not policy. */
53
- function propDefaults(props?: Record<string, unknown>): Record<string, unknown> {
54
- const out: Record<string, unknown> = {};
55
- for (const [k, v] of Object.entries(props ?? {})) {
56
- if (v && typeof v === "object" && "default" in (v as Record<string, unknown>)) {
57
- out[k] = (v as { default: unknown }).default;
58
- }
59
- }
60
- return out;
61
- }
62
-
63
- /** Format a turn timestamp as relative copy ("just now" / "5 min ago"). Neutral
64
- * projection of a model value (like `text`), not UX policy. Mirrors legacy
65
- * ChatHistoryItem.formatRelativeTime. Empty string when unstamped. */
66
- function formatRelative(ts?: number): string {
67
- if (!ts) return "";
68
- const diff = Math.floor((Date.now() - ts) / 1000);
69
- if (diff < 60) return "just now";
70
- if (diff < 3600) return `${Math.floor(diff / 60)} min ago`;
71
- if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`;
72
- return new Date(ts).toLocaleDateString();
73
- }
74
-
75
- /**
76
- * Walk a template tree and collect every VIEW a reaction surface claims — the `eq`
77
- * value of each `ComponentSlot.select.where { field: "defaultState" }`. State-selected
78
- * UI (STATE_MODEL §5b): a component instance renders in exactly ONE placeholder. If a
79
- * surface shows a given view, the conversation flow (the `inline` placeholder) must NOT
80
- * also render instances in that view — else the same instance paints twice. A view no
81
- * surface claims is absent from this set, so it stays in the flow (the fallback).
82
- * Exported for testing.
83
- */
84
- export function collectSurfacedViews(node: unknown, out: Set<string> = new Set()): Set<string> {
85
- if (Array.isArray(node)) {
86
- for (const n of node) collectSurfacedViews(n, out);
87
- return out;
88
- }
89
- if (!node || typeof node !== "object") return out;
90
- const n = node as Record<string, unknown>;
91
- const sel = n.select as { where?: { field?: string; eq?: unknown } } | undefined;
92
- if (n.type === "ComponentSlot" && sel?.where?.field === "defaultState" && typeof sel.where.eq === "string") {
93
- out.add(sel.where.eq);
94
- }
95
- for (const v of Object.values(n)) if (v && typeof v === "object") collectSurfacedViews(v, out);
96
- return out;
97
- }
98
-
99
- /**
100
- * STATE-OWNED APP WIDTH (docs/design/05, layouts era): the app's outer width is
101
- * declared by its LAYOUT's data, never the manifest. Two declaration points, both
102
- * the neutral `appWidth` key (a named app size resolved via theme.appSize):
103
- * - a plain node's `appWidth` = an always-open panel (the core column) — the
104
- * layout's constant term.
105
- * - a ComponentSlot surface's `appWidth` = its panel width, counted only while
106
- * the surface is OCCUPIED (its view is the active state).
107
- * The caller passes the ACTIVE layout (resolveActiveLayout) — layout swaps are
108
- * whole-arrangement swaps, so the width is always the active layout's total.
109
- * Activation is DERIVED, never stored. No declared width → undefined: the host
110
- * keeps its own default, the sizing twin of "unmatched view falls back to inline".
111
- * There is no other path.
112
- */
113
- export function computeAppWidth(root: unknown, store: ComponentStore, appSize?: Record<string, string>): string | undefined {
114
- // Resolve a declared width: a NAMED SIZE via theme.appSize (like any token), else raw
115
- // CSS — guarded so an unresolvable name can never emit invalid CSS.
116
- const rw = (w: unknown): string | undefined => cssWidth(typeof w === "string" ? (appSize?.[w] ?? w) : undefined);
117
- const r = root as Record<string, unknown> | null | undefined;
118
- const widths: string[] = [];
119
- // Gather the tree's width-bearing nodes: always-open core panels and the reaction
120
- // SURFACES (view → panel width).
121
- const surfaces = new Map<string, string | undefined>();
122
- const walk = (node: unknown): void => {
123
- if (Array.isArray(node)) {
124
- for (const c of node) walk(c);
125
- return;
126
- }
127
- if (!node || typeof node !== "object") return;
128
- const n = node as Record<string, unknown> & { appWidth?: string; type?: string; select?: { where?: { field?: string; eq?: unknown } } };
129
- if (n !== r) {
130
- if (n.type === "ComponentSlot") {
131
- const claim = n.select?.where;
132
- // `appWidth: flex` — the surface takes the REMAINING host space. DATA WINS: an
133
- // org may define `flex` as a real appSize token (bpp: calc(100% − chat)) and
134
- // that resolves like any name; the marker below is the theme-free fallback,
135
- // resolved as "fill the host" when the view is active.
136
- if (claim?.field === "defaultState" && typeof claim.eq === "string")
137
- surfaces.set(claim.eq, rw(n.appWidth) ?? (n.appWidth === "flex" ? "flex" : undefined));
138
- } else if (n.appWidth !== undefined) {
139
- const w = rw(n.appWidth);
140
- if (w) widths.push(w); // a plain panel (the core column) is ALWAYS open
141
- }
142
- }
143
- for (const v of Object.values(n)) if (v && typeof v === "object") walk(v);
144
- };
145
- walk(r);
146
- // ONE STATE AT A TIME (docs/design/04): the app = core + the ACTIVE state's panel —
147
- // the most recently surfaced view. Panel combinations cannot exist, so the width is
148
- // always one of a small known set, bounded by construction.
149
- let active = "";
150
- let activeAt = -1;
151
- for (const turn of store.getResponses())
152
- for (const p of turn.components) {
153
- const { chatId, nodeId } = store.split(p);
154
- const view = (store.get(chatId, nodeId) as Record<string, unknown>)?.defaultState;
155
- if (typeof view === "string" && surfaces.has(view) && store.touchOf(p) > activeAt) {
156
- activeAt = store.touchOf(p);
157
- active = view;
158
- }
159
- }
160
- const activePanel = active ? surfaces.get(active) : undefined;
161
- // An ACTIVE flex surface fills the host viewport (the host caps at its pane); the
162
- // surface flexes into whatever the fixed panels leave.
163
- if (activePanel === "flex") return "100%";
164
- if (activePanel) widths.push(activePanel);
165
- if (!widths.length) return undefined;
166
- return widths.length === 1 ? widths[0] : `calc(${widths.join(" + ")})`;
167
- }
168
-
169
- /**
170
- * TEMPLATE LAYOUTS — name-sync (docs/design/05). A template with `layouts` is a set
171
- * of full arrangements, the template-tier twin of a component's faces: the ACTIVE
172
- * layout is the one whose NAME matches the latest surfaced component view, falling
173
- * back to `defaultLayout` when nothing (or nothing matching) is surfaced. Same
174
- * derivation as the active state — read off the store, never stored. Single-layout
175
- * templates (`layouts` absent) resolve to `root`: the classic behavior, untouched.
176
- */
177
- export function resolveActiveLayout(def: UnoverseDefinition | null | undefined, store: ComponentStore): UnoverseNode | undefined {
178
- if (!def) return undefined;
179
- const layouts = def.layouts;
180
- if (!layouts) return def.root;
181
- const { view } = resolveActiveView(def, store, new Set(Object.keys(layouts)));
182
- return layouts[view] ?? layouts[def.defaultLayout ?? "main"] ?? def.root;
183
- }
184
-
185
- /**
186
- * THE ACTIVE VIEW — the ONE derivation every consumer shares (docs/design/04 + the
187
- * guest-control law). The latest surfaced component view among `validViews`, with
188
- * PINNED enforcement: while a pinned layout's view is surfaced, a background arrival
189
- * cannot displace it — only a guest write anywhere can. Layout choice and surface
190
- * gating MUST both read this function: two loops that each pick "the latest view"
191
- * disagree the moment pin overrides one of them — the layout renders view A while
192
- * every surface inside it gates against view B, and the whole panel goes blank.
193
- */
194
- export function resolveActiveView(
195
- def: UnoverseDefinition | null | undefined,
196
- store: ComponentStore,
197
- validViews: Set<string>,
198
- ): { view: string; byGuest: boolean } {
199
- let view = "";
200
- let at = -1;
201
- let byGuest = false;
202
- let pinnedView = "";
203
- let pinnedAt = -1;
204
- for (const turn of store.getResponses())
205
- for (const p of turn.components) {
206
- const { chatId, nodeId } = store.split(p);
207
- const v = (store.get(chatId, nodeId) as Record<string, unknown>)?.defaultState;
208
- if (typeof v !== "string" || !validViews.has(v)) continue;
209
- const t = store.touchOf(p);
210
- if (t > at) {
211
- at = t;
212
- view = v;
213
- byGuest = store.interactionTouchOf(p) >= t;
214
- }
215
- if ((def?.layouts?.[v] as { pinned?: boolean } | undefined)?.pinned && t > pinnedAt) {
216
- pinnedAt = t;
217
- pinnedView = v;
218
- }
219
- }
220
- if (pinnedView && view !== pinnedView && !byGuest) return { view: pinnedView, byGuest: false };
221
- return { view, byGuest };
222
- }
223
-
224
- /** Resolve a ComponentSlot's `select` against the store → ordered pointers. Exported for testing. */
225
- export function selectPointers(store: ComponentStore, node: UnoverseNode): string[] {
226
- const sel = node.select ?? {};
227
- let pointers =
228
- sel.from === "all"
229
- ? store.getResponses().flatMap((r) => r.components)
230
- : (store.latestResponse()?.components ?? []);
231
-
232
- // `where` — select by COMPONENT STATE (the reaction contract, STATE_MODEL §5b): any
233
- // component whose own slice matches, ordered newest state-write first so `limit: 1`
234
- // = "most recent wins". The generic replacement for type-pinned surfaces — a
235
- // template reacts to whatever component enters the state, never to a named type.
236
- if (sel.where) {
237
- const { field, eq, ne } = sel.where;
238
- pointers = pointers.filter((p) => {
239
- const { chatId, nodeId } = store.split(p);
240
- const v = (store.get(chatId, nodeId) as Record<string, unknown>)[field];
241
- return ne !== undefined ? v !== ne : v === eq;
242
- });
243
- pointers = [...pointers].sort((a, b) => store.touchOf(b) - store.touchOf(a));
244
- }
245
- if (sel.limit != null) pointers = pointers.slice(0, sel.limit);
246
- return pointers;
247
- }
248
-
249
74
  export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme: themeProp, props, isolate = true, voice: voiceConfig }: StreamedUnoverseTemplateProps) {
250
75
  const [def, setDef] = useState<UnoverseDefinition | null>(null);
251
76
  const [error, setError] = useState<string | null>(null);
@@ -271,7 +96,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
271
96
  // to their own slices — so streaming chunks never re-walk the whole template tree.
272
97
  // Stable subscribe fn: an inline arrow would tear down + re-create the subscription
273
98
  // every render.
274
- useSyncExternalStore(
99
+ const structureVersion = useSyncExternalStore(
275
100
  useCallback((cb) => store.subscribe(cb), [store]),
276
101
  useCallback(() => store.getStructureVersion(), [store]),
277
102
  );
@@ -289,18 +114,51 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
289
114
  };
290
115
  }, [client, uri]);
291
116
 
292
- // NAME-SYNC: the tree this template presents — the layout matching the latest
293
- // surfaced component view (multi-layout templates), else the classic root. The
294
- // resolved node is a stable per-def object, so downstream memos key off it.
117
+ // ARRIVAL PLACEMENT (STATE_MODEL v2 §5 rule 3): nobody sends a state — the HOST
118
+ // places the actor at spawn. For each arrived component whose slice has no view yet:
119
+ // 1. the component's DECLARED INITIAL, if this template has a state for it (the
120
+ // component's preference stands whenever the host can honor it);
121
+ // 2. else the SCAN — the first name in THIS template's declared order that the
122
+ // component's public menu (`publicStates`, server-injected at init) also has;
123
+ // 3. no overlap at all → leave unset: the component wakes in its own initial and
124
+ // rides inline (rule 6).
125
+ // The write is a data merge (never an interaction touch — placement is not the
126
+ // guest's hand), it happens ONCE per instance (a placed slice has a view), and the
127
+ // component owns the axis from then on: the template never writes it again.
128
+ useEffect(() => {
129
+ const order = def?.stateOrder;
130
+ if (!order?.length) return;
131
+ for (const turn of store.getResponses())
132
+ for (const p of turn.components) {
133
+ const { chatId, nodeId } = store.split(p);
134
+ const slice = store.get(chatId, nodeId) as Record<string, unknown> | undefined;
135
+ if (!slice || viewOf(slice) !== undefined) continue;
136
+ const menu = Array.isArray(slice.publicStates) ? (slice.publicStates as unknown[]) : undefined;
137
+ if (!menu) continue;
138
+ const initial = typeof slice.initialView === "string" ? slice.initialView : undefined;
139
+ const face = initial && order.includes(initial) ? initial : order.find((s) => menu.includes(s));
140
+ if (face) store.apply({ type: "COMPONENT_DATA", chatId, nodeId, data: { view: face } });
141
+ }
142
+ }, [def, store, structureVersion]);
143
+
144
+ // THE SEVEN RULES (2026-08-09) need no effect here: a higher state MASKS the ones
145
+ // below (rule 4 — the ladder alone decides what shows; lower states refresh
146
+ // silently underneath via the store's arrival supersession), and releasing the top
147
+ // state is just the ladder looking again (rule 6). The earlier cancellation WRITE
148
+ // (cancelBelow) was superseded by this pure-masking ruling and deleted.
149
+
150
+ // STATE OWNS LAYOUT (STATE_MODEL v2): the tree this template presents — the active
151
+ // state's layout (multi-layout templates), else the classic root. The resolved node
152
+ // is a stable per-def object, so downstream memos key off it.
295
153
  const activeRoot = def ? resolveActiveLayout(def, store) : undefined;
296
154
 
297
- // The set of VIEWS this template's reaction surfaces claim — computed per active
298
- // layout: layout NAMES claim their view (name-sync) alongside the surfaces inside.
299
- // The timeline uses it to keep a claimed instance out of the flow (one instance → one
300
- // placeholder).
155
+ // The CLAIM set — the states this template reacts to. v2: the declared order
156
+ // (stateOrder) IS the claim list; legacy defs fall back to layout names + surface
157
+ // claims. The timeline uses it to keep a claimed instance out of the flow (one
158
+ // instance → one placeholder).
301
159
  const surfacedViews = useMemo(() => {
302
- const views = activeRoot ? collectSurfacedViews(activeRoot) : new Set<string>();
303
- for (const name of Object.keys(def?.layouts ?? {})) views.add(name);
160
+ const views = new Set<string>(orderedClaims(def));
161
+ if (activeRoot) collectSurfacedViews(activeRoot, views, def?.defs as Record<string, unknown> | undefined);
304
162
  return views;
305
163
  }, [activeRoot, def]);
306
164
 
@@ -326,16 +184,17 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
326
184
  if (action === "backToChat") voice.endCall();
327
185
  }
328
186
  // GUEST CLOSE from TEMPLATE CHROME: a ✕ on the layout itself (a rail/panel header —
329
- // not inside a single card) fires the CloseButton atom's `setValue defaultState:"inline"`.
330
- // Template chrome has no card slice to write, so that write would be inert; instead it
331
- // means "un-surface the current view" → the store retracts every surface to base (the
332
- // same reset a new turn runs). A card that closes ITSELF keeps using the plain component
333
- // write (real ids), untouched. `then` still chains via dispatchAction below.
187
+ // not inside a single card) fires the CloseButton atom's `setValue view:"inline"`
188
+ // (legacy defs write `defaultState`). Template chrome has no card slice to write, so
189
+ // that write would be inert; instead it means "un-surface the current view" → the
190
+ // store retracts every surface to its declared base (the same reset a new turn
191
+ // runs). A card that closes ITSELF keeps using the plain component write (real
192
+ // ids), untouched. `then` still chains via dispatchAction below.
334
193
  if (
335
194
  action && typeof action === "object" &&
336
195
  (action as { type?: string }).type === "setValue" &&
337
196
  (action as { values?: { key?: string; value?: unknown }[] }).values?.some(
338
- (v) => v.key === "defaultState" && v.value === "inline",
197
+ (v) => isViewField(v.key) && v.value === "inline",
339
198
  )
340
199
  ) {
341
200
  store.closeSurfaces();
@@ -375,12 +234,12 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
375
234
  // data is untouched and re-presents the moment its state is active again.
376
235
  // Surfaces never stack; panel combinations cannot exist.
377
236
  const claim = node.select?.where;
378
- if (claim?.field === "defaultState" && typeof claim.eq === "string" && claim.eq !== surfacedView) {
379
- return node.fallback ? renderNode(node.fallback, {}, dispatch, theme, key ?? "fallback") : null;
237
+ if (isViewField(claim?.field) && typeof claim?.eq === "string" && claim.eq !== surfacedView) {
238
+ return node.fallback ? renderNode(node.fallback, {}, dispatch, theme, key ?? "fallback", undefined, def?.defs) : null;
380
239
  }
381
240
  const pointers = selectPointers(store, node);
382
241
  if (pointers.length === 0) {
383
- return node.fallback ? renderNode(node.fallback, {}, dispatch, theme, key ?? "fallback") : null;
242
+ return node.fallback ? renderNode(node.fallback, {}, dispatch, theme, key ?? "fallback", undefined, def?.defs) : null;
384
243
  }
385
244
  // `frame` — chrome that exists ONLY because the slot matched (fallback's twin).
386
245
  // The frame subtree renders with a slot resolver that serves the outer slot's
@@ -403,7 +262,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
403
262
  const frame = appWidth
404
263
  ? ({ ...node.frame, style: { ...(node.frame.style ?? {}), width: appWidth, flex: "0 0 auto" } } as UnoverseNode)
405
264
  : node.frame;
406
- return renderNode(frame, store.getTemplateState() as Record<string, unknown>, dispatch, theme, key ?? "frame", innerSlot);
265
+ return renderNode(frame, store.getTemplateState() as Record<string, unknown>, dispatch, theme, key ?? "frame", innerSlot, def?.defs);
407
266
  }
408
267
  return pointers.map((p) => leaf(p));
409
268
  };
@@ -427,15 +286,15 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
427
286
  turn.role === "assistant"
428
287
  ? turn.components.filter((p) => {
429
288
  const { chatId, nodeId } = store.split(p);
430
- const view = (store.get(chatId, nodeId) as Record<string, unknown> | undefined)?.defaultState;
431
- return typeof view !== "string" || !surfacedViews.has(view);
289
+ const view = viewOf(store.get(chatId, nodeId) as Record<string, unknown> | undefined);
290
+ return !view || !surfacedViews.has(view);
432
291
  })
433
292
  : [];
434
293
  // Slot scoped to this turn's UNCLAIMED components (assistant turns only).
435
294
  const turnSlot: SlotResolver = (slotNode, k) => {
436
295
  let ptrs = [...shown];
437
296
  if (slotNode.select?.limit != null) ptrs = ptrs.slice(0, slotNode.select.limit);
438
- if (ptrs.length === 0) return slotNode.fallback ? renderNode(slotNode.fallback, {}, dispatch, theme, k ?? "fb") : null;
297
+ if (ptrs.length === 0) return slotNode.fallback ? renderNode(slotNode.fallback, {}, dispatch, theme, k ?? "fb", undefined, def?.defs) : null;
439
298
  // Pass the turn's streaming state into each component's scope so a streamed
440
299
  // component (e.g. StreamingText) can stop its own loading dots when complete.
441
300
  const streaming = turn.role === "assistant" && turn.streamingState === "streaming";
@@ -463,7 +322,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
463
322
  // inactive → the definition hides it (legacy ChatHistoryItem null-returns).
464
323
  active: turn.streamingState === "streaming" || shown.length > 0,
465
324
  };
466
- return renderNode(sub, data, dispatch, theme, turn.id, turnSlot);
325
+ return renderNode(sub, data, dispatch, theme, turn.id, turnSlot, def?.defs);
467
326
  })}
468
327
  </div>
469
328
  );
@@ -505,7 +364,10 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
505
364
  const keyframes = <style>{keyframesCss(theme)}</style>;
506
365
  // A layout root never carries `appWidth` (guard-enforced) — panels inside it do.
507
366
  const rootNode = (activeRoot ?? def.root) as UnoverseNode;
508
- const tree = renderNode(rootNode, rootData, dispatch, theme, undefined, resolve);
367
+ // The template's own shared pieces, threaded into every branch it draws: chrome, a
368
+ // slot's fallback, a timeline turn. A reference in any of them resolves against the
369
+ // same map rather than rendering nothing.
370
+ const tree = renderNode(rootNode, rootData, dispatch, theme, undefined, resolve, def?.defs);
509
371
  // A template is a SURFACE: its render-root fills the box the host gives it, so a root
510
372
  // that declares `height: "full"` (DATA) can resolve. This only fills when the host's box
511
373
  // is definite-height; when the host sizes to content the 100% resolves to content — so a
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: unoverse-create
3
- description: Create or edit Unoverse platform artifacts — UI components (including briefed/AI-filled components), templates, atoms and styles (rx/), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use when the user wants to create, add, build, or modify a component, template, theme, agent skill, prompt block, custom node, or workflow in an Unoverse starter repo.
3
+ description: Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), templates, atoms and styles (rx/), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use when the user wants to create, add, build, or modify a component, template, theme, agent skill, prompt block, custom node, or workflow in an Unoverse starter repo.
4
4
  ---
5
5
 
6
6
  # Creating Unoverse Artifacts
7
7
 
8
- You are helping a developer build in the **Unoverse developer carve-out** — the three
8
+ You are helping a developer build in the **Unoverse developer carve-out**, the three
9
9
  editable folders mounted into the running platform:
10
10
 
11
11
  | Folder | What lives there |
@@ -14,14 +14,14 @@ editable folders mounted into the running platform:
14
14
  | `apps/unoverse/prompts/` | **Behavior**: agent skills (`skills/`) + prompt blocks (`blocks/`) |
15
15
  | `apps/unoverse/nodes/` | **Logic**: custom workflow nodes, authored as YAML |
16
16
 
17
- ## Step 1 — Identify the artifact, read its playbook
17
+ ## Step 1: Identify the artifact, read its playbook
18
18
 
19
19
  Decide what the user is creating, then **read the matching reference file before writing
20
20
  anything**:
21
21
 
22
22
  | The user wants… | Read |
23
23
  |---|---|
24
- | A UI component or shared atom — incl. a page/card the AI fills with content (briefed) | `references/component.md` (§Briefs for AI-filled) |
24
+ | A UI component or shared atom, incl. a page/card the AI fills with content (briefed) | `references/component.md` (§Briefs for AI-filled) |
25
25
  | A template / app layout (chat surface, wizard shell) | `references/template.md` |
26
26
  | A style / theme / token change | `references/component.md` §Tokens |
27
27
  | An agent skill (behavior guide the AI follows) | `references/agent-skill.md` |
@@ -30,11 +30,11 @@ anything**:
30
30
  | A workflow (wire nodes into an agent/pipeline on the Canvas) | `references/workflow.md` |
31
31
 
32
32
  Workflows are the one artifact that is NOT files: they are built live on the running
33
- platform through the `canvas` MCP (registered by this repo's `.mcp.json`) —
34
- the playbook covers the bind/build/test contract.
33
+ platform through the `canvas` MCP (registered by this repo's `.mcp.json`).
34
+ The playbook covers the bind/build/test contract.
35
35
 
36
36
  If it's ambiguous (e.g. "add a card that shows weather"), it's usually a **component**
37
- (the UI) plus possibly a **node** (the data source) — confirm the scope with the user.
37
+ (the UI) plus possibly a **node** (the data source). Confirm the scope with the user.
38
38
 
39
39
  **New to design artifacts?** The guided learning journey at `docs/design/` (README →
40
40
  quick-start → … → troubleshooting) teaches components, templates, the state model
@@ -42,32 +42,32 @@ quick-start → … → troubleshooting) teaches components, templates, the stat
42
42
  `docs/design/CLAUDE.md` is the condensed agent rulebook. The playbooks here assume that
43
43
  material.
44
44
 
45
- ## Step 2 — Hard rules (apply to every artifact)
45
+ ## Step 2: Hard rules (apply to every artifact)
46
46
 
47
47
  1. **Only edit the three carve-out folders.** Never touch, vendor, or work around the
48
- SDK, engine, or server — they are not yours to change. If a task seems to require an
49
- SDK change, the answer is almost always "express it as data" — re-read the playbook.
48
+ SDK, engine, or server. They are not yours to change. If a task seems to require an
49
+ SDK change, the answer is almost always "express it as data". Re-read the playbook.
50
50
  2. **Docs are the source of truth.** The playbooks name the design journey and the deep
51
51
  references ([UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md), [UNOVERSE_STATE_MODEL](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_STATE_MODEL.md), [UNOVERSE_LAYERS](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_LAYERS.md),
52
52
  [UNOVERSE_CONFORMANCE](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_CONFORMANCE.md)). They live in the docs repo
53
53
  (https://github.com/unoverse-platform/docs), not in your repo. When the user's request goes beyond the playbook, read the named section rather
54
54
  than improvising conventions.
55
- 3. **UI is data.** No pixels, hex colors, or CSS anywhere — token names only. No logic
56
- in definitions — anything computed (totals, formatting, chosen colors) is computed in
55
+ 3. **UI is data.** No pixels, hex colors, or CSS anywhere: token names only. No logic
56
+ in definitions: anything computed (totals, formatting, chosen colors) is computed in
57
57
  the node and sent as a plain field.
58
58
  4. **Audit before you're done.** Every component/template must pass the conformance
59
59
  checklist ([UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §9). Walk it item by item.
60
- 5. **Discovery meta is ranked, not read.** Any spatially-discoverable artifact — a
61
- component or template with a manifest, an agent skill, a node — is selected by
60
+ 5. **Discovery meta is ranked, not read.** Any spatially-discoverable artifact (a
61
+ component or template with a manifest, an agent skill, a node) is selected by
62
62
  embedding `title`/`name` + `whenToUse`/`description` + `category` against the user's
63
63
  own words (or the planner's task). The strict contract is
64
64
  `docs/nodes/node-discoverability.md`: outcome-first, utterance-shaped, opening
65
65
  words dominate, disqualify by property (never name a sibling). Each playbook carries
66
66
  the summary; read doc 14 before writing any meta.
67
67
 
68
- ## Step 3 — Deploy loop (after the artifact is written)
68
+ ## Step 3: Deploy loop (after the artifact is written)
69
69
 
70
- Components and templates have NO scaffold command — author them from the references
70
+ Components and templates have NO scaffold command. Author them from the references
71
71
  (mirror the closest existing artifact). Validation lives in Studio: its publish step
72
72
  lints every definition before anything leaves the machine (0 errors required, doc-cited
73
73
  messages; justify any warning), and the conformance checklist is
@@ -75,7 +75,7 @@ messages; justify any warning), and the conformance checklist is
75
75
 
76
76
  | Artifact | To see it live |
77
77
  |---|---|
78
- | Component / atom / template / style | Preview in Studio (`unoverse studio`), then publish from Studio — the publish lint gates it |
78
+ | Component / atom / template / style | Preview in Studio (`unoverse studio`), then publish from Studio: the publish lint gates it |
79
79
  | Existing component restyle/edit only | takes effect live (the SDK reads `rx/` directly) |
80
80
  | Agent skill / prompt block | publish from Studio, same gate |
81
81
  | Node | Studio's Nodes screen: Load sample → Run. A node is YAML; running it is the only proof (no build) |
@@ -85,4 +85,4 @@ on :4108 (`unoverse studio` from the project folder).
85
85
 
86
86
  > In the platform monorepo (not the starter), the node docs live at
87
87
  > `packages/docs/nodes/`, the design journey at `packages/docs/design/`, and the dev loop
88
- > is just `npm run dev` — component nodes synthesize from rx/ definitions at boot.
88
+ > is just `npm run dev`. Component nodes synthesize from rx/ definitions at boot.
@@ -1,4 +1,4 @@
1
- # Playbook — Agent Skills (prompts/skills)
1
+ # Playbook: Agent Skills (prompts/skills)
2
2
 
3
3
  An agent skill is a markdown behavior guide the platform's AI agents discover and follow
4
4
  at runtime (e.g. how to handle complaints, how to walk a user through a process). It is
@@ -8,7 +8,7 @@ at runtime (e.g. how to handle complaints, how to walk a user through a process)
8
8
 
9
9
  ```
10
10
  apps/unoverse/prompts/skills/<skill-name>/
11
- SKILL.md # required — frontmatter + instructions
11
+ SKILL.md # required: frontmatter + instructions
12
12
  ```
13
13
 
14
14
  The runtime scans this folder and serves skills to agents; agents match on the
@@ -19,9 +19,9 @@ The runtime scans this folder and serves skills to agents; agents match on the
19
19
  ```markdown
20
20
  ---
21
21
  name: skill-name # required: lowercase, numbers, hyphens; max 64 chars
22
- title: Skill Name # human display name — shows as the item's title in listings
23
- description: What this skill IS — ONE short line (≤ 120 chars), the listing subtitle
24
- whenToUse: The SELECTION text — outcome-first, in the user's vocabulary; ranked against intent for spatial discovery. See docs/nodes/node-discoverability.md — the node rules apply verbatim.
22
+ title: Skill Name # human display name, shown as the item's title in listings
23
+ description: What this skill IS: ONE short line (≤ 120 chars), the listing subtitle
24
+ whenToUse: The SELECTION text: outcome-first, in the user's vocabulary; ranked against intent for spatial discovery. See docs/nodes/node-discoverability.md. The node rules apply verbatim.
25
25
  version: 1.0.0 # optional, semver
26
26
  category: ai # optional: ai | integration | workflow | utility
27
27
  triggers: # optional, max 20 keywords/phrases (2–50 chars each)
@@ -36,24 +36,24 @@ nodeTypes: [] # optional: workflow node types the skill relies
36
36
  [The situations that should activate this behavior]
37
37
 
38
38
  ## Approach / Instructions
39
- [Clear, step-by-step guidance. Concrete do/don't phrasing beats abstractions —
40
- include example lines the agent can use verbatim where tone matters.]
39
+ [Clear, step-by-step guidance. Concrete do/don't phrasing beats abstractions.
40
+ Include example lines the agent can use verbatim where tone matters.]
41
41
 
42
42
  ## Examples
43
43
  [Concrete before/after or dialogue examples]
44
44
  ```
45
45
 
46
- Stick to the keys above. The markdown body must exist — a skill with frontmatter only
46
+ Stick to the keys above. The markdown body must exist: a skill with frontmatter only
47
47
  is invalid.
48
48
 
49
49
  ## Writing rules
50
50
 
51
51
  1. **`whenToUse` does the routing; `description` says what it is.** Never blend them
52
52
  into one field. `whenToUse` is embedded and ranked against user intent (spatial
53
- discovery / findIntent) — write it **outcome-first in the user's vocabulary**
54
- ("Handle an unhappy customer — a complaint, frustration, or a request to
53
+ discovery / findIntent). Write it **outcome-first in the user's vocabulary**
54
+ ("Handle an unhappy customer: a complaint, frustration, or a request to
55
55
  escalate…"), never mechanism-first. The full rules are
56
- `docs/nodes/node-discoverability.md` — they apply to skills verbatim.
56
+ `docs/nodes/node-discoverability.md`. They apply to skills verbatim.
57
57
  2. **Study an existing skill** in `prompts/skills/` and match its voice and structure.
58
58
  3. Keep instructions outcome-focused and scannable: short sections, bulleted do/don't
59
59
  lists, explicit escalation/stop conditions.
@@ -61,5 +61,5 @@ is invalid.
61
61
 
62
62
  ## Ship
63
63
 
64
- `docker compose restart unoverse` — the runtime rescans skills at boot. Verify the
64
+ `docker compose restart unoverse`. The runtime rescans skills at boot. Verify the
65
65
  skill appears via the platform's skill listing (`unoverse check` for overall health).
@@ -1,6 +1,6 @@
1
- # Playbook — Prompt Blocks (prompts/blocks)
1
+ # Playbook: Prompt Blocks (prompts/blocks)
2
2
 
3
- A prompt block is a **reusable prompt fragment** — a named piece of agent prompt text
3
+ A prompt block is a **reusable prompt fragment**: a named piece of agent prompt text
4
4
  (formatting rules, a reasoning pattern, a policy) that workflows compose into agent
5
5
  system prompts. Smaller than a skill: a skill is a discoverable behavior guide; a block
6
6
  is an ingredient.
@@ -23,7 +23,7 @@ description: What this block contributes to a prompt
23
23
  tags: [topic, topic2]
24
24
  ---
25
25
 
26
- [The prompt text itself — written as direct instructions to the agent,
26
+ [The prompt text itself, written as direct instructions to the agent,
27
27
  present tense, no meta-commentary about being a block.]
28
28
  ```
29
29
 
@@ -31,7 +31,7 @@ present tense, no meta-commentary about being a block.]
31
31
 
32
32
  1. **Study an existing block** (e.g. `blocks/formatting/markdown-guidelines.md`) and
33
33
  match its shape: tight sections, imperative bullets.
34
- 2. A block must stand alone — no references to other blocks, no assumptions about what
34
+ 2. A block must stand alone: no references to other blocks, no assumptions about what
35
35
  else is in the prompt.
36
36
  3. Keep it generic and reusable. Anything workflow-specific belongs in that workflow's
37
37
  own prompt, not in a shared block.
@@ -40,4 +40,4 @@ present tense, no meta-commentary about being a block.]
40
40
 
41
41
  ## Ship
42
42
 
43
- `docker compose restart unoverse` — the runtime rescans blocks at boot.
43
+ `docker compose restart unoverse`. The runtime rescans blocks at boot.