@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.
- package/package.json +1 -1
- package/screens/AtomsView.tsx +40 -2
- package/screens/ComponentsView.tsx +112 -21
- package/screens/NativeAppHost.tsx +8 -4
- package/screens/TemplatesView.tsx +51 -21
- package/screens/states.ts +27 -4
- package/vendor/sdk/appEngine.tsx +20 -4
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -2
- package/vendor/sdk/lib/core/actions.ts +10 -6
- package/vendor/sdk/lib/core/client.ts +39 -12
- package/vendor/sdk/lib/core/index.ts +21 -0
- package/vendor/sdk/lib/core/refs.ts +117 -0
- package/vendor/sdk/lib/core/store.ts +65 -38
- package/vendor/sdk/lib/core/templates.ts +270 -0
- package/vendor/sdk/lib/core/types.ts +27 -7
- package/vendor/sdk/lib/orb/ThinkingOrb.tsx +131 -0
- package/vendor/sdk/lib/orb/engine/braid.ts +50 -0
- package/vendor/sdk/lib/orb/engine/core.ts +167 -0
- package/vendor/sdk/lib/orb/engine/lattice.ts +205 -0
- package/vendor/sdk/lib/orb/engine/morph.ts +138 -0
- package/vendor/sdk/lib/orb/engine/orbits.ts +80 -0
- package/vendor/sdk/lib/orb/engine/profiles.ts +162 -0
- package/vendor/sdk/lib/orb/engine/registry.ts +31 -0
- package/vendor/sdk/lib/orb/engine/ribbon.ts +93 -0
- package/vendor/sdk/lib/orb/engine/rings.ts +239 -0
- package/vendor/sdk/lib/orb/engine/types.ts +14 -0
- package/vendor/sdk/lib/orb/engine/web.ts +98 -0
- package/vendor/sdk/lib/orb/index.ts +20 -0
- package/vendor/sdk/lib/orb/presets.ts +181 -0
- package/vendor/sdk/lib/orb/theme.ts +83 -0
- package/vendor/sdk/lib/orb/types.ts +84 -0
- package/vendor/sdk/lib/primitives.tsx +116 -2
- package/vendor/sdk/lib/render.tsx +32 -8
- package/vendor/sdk/lib/style.ts +6 -2
- package/vendor/sdk/lib/template.tsx +86 -224
- package/vendor/skills/unoverse-create/SKILL.md +18 -18
- package/vendor/skills/unoverse-create/references/agent-skill.md +12 -12
- package/vendor/skills/unoverse-create/references/block.md +5 -5
- package/vendor/skills/unoverse-create/references/component.md +150 -75
- package/vendor/skills/unoverse-create/references/node.md +1 -1
- package/vendor/skills/unoverse-create/references/template.md +142 -82
- 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 {
|
|
17
|
-
|
|
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
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
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
|
|
298
|
-
//
|
|
299
|
-
// The timeline uses it to keep a claimed instance out of the flow (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 =
|
|
303
|
-
|
|
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
|
|
330
|
-
// Template chrome has no card slice to write, so
|
|
331
|
-
// means "un-surface the current view" → the
|
|
332
|
-
//
|
|
333
|
-
//
|
|
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
|
|
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
|
|
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)
|
|
431
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
49
|
-
SDK change, the answer is almost always "express it as data"
|
|
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
|
|
56
|
-
in definitions
|
|
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
|
|
61
|
-
component or template with a manifest, an agent skill, a node
|
|
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
|
|
68
|
+
## Step 3: Deploy loop (after the artifact is written)
|
|
69
69
|
|
|
70
|
-
Components and templates have NO scaffold command
|
|
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
|
|
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
|
|
88
|
+
> is just `npm run dev`. Component nodes synthesize from rx/ definitions at boot.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Playbook
|
|
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
|
|
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
|
|
23
|
-
description: What this skill IS
|
|
24
|
-
whenToUse: The SELECTION text
|
|
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
|
-
|
|
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
|
|
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)
|
|
54
|
-
("Handle an unhappy customer
|
|
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
|
|
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
|
|
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
|
|
1
|
+
# Playbook: Prompt Blocks (prompts/blocks)
|
|
2
2
|
|
|
3
|
-
A prompt block is a **reusable prompt fragment
|
|
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
|
|
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
|
|
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
|
|
43
|
+
`docker compose restart unoverse`. The runtime rescans blocks at boot.
|