@unoverse-platform/studio 0.1.25 → 0.1.27

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 (45) 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/SourceView.tsx +1 -1
  6. package/screens/TemplatesView.tsx +52 -22
  7. package/screens/states.ts +27 -4
  8. package/src/PublishDialog.tsx +1 -1
  9. package/vendor/sdk/appEngine.tsx +44 -3
  10. package/vendor/sdk/lib/UnoverseComponent.tsx +1 -1
  11. package/vendor/sdk/lib/core/actions.ts +20 -6
  12. package/vendor/sdk/lib/core/analytics.ts +89 -0
  13. package/vendor/sdk/lib/core/client.ts +1 -1
  14. package/vendor/sdk/lib/core/index.ts +30 -0
  15. package/vendor/sdk/lib/core/store.ts +65 -38
  16. package/vendor/sdk/lib/core/templates.ts +252 -0
  17. package/vendor/sdk/lib/core/types.ts +40 -7
  18. package/vendor/sdk/lib/index.ts +9 -0
  19. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +127 -0
  20. package/vendor/sdk/lib/orb/engine/braid.ts +50 -0
  21. package/vendor/sdk/lib/orb/engine/core.ts +157 -0
  22. package/vendor/sdk/lib/orb/engine/lattice.ts +205 -0
  23. package/vendor/sdk/lib/orb/engine/morph.ts +138 -0
  24. package/vendor/sdk/lib/orb/engine/orbits.ts +80 -0
  25. package/vendor/sdk/lib/orb/engine/profiles.ts +162 -0
  26. package/vendor/sdk/lib/orb/engine/registry.ts +24 -0
  27. package/vendor/sdk/lib/orb/engine/ribbon.ts +93 -0
  28. package/vendor/sdk/lib/orb/engine/types.ts +14 -0
  29. package/vendor/sdk/lib/orb/engine/web.ts +98 -0
  30. package/vendor/sdk/lib/orb/index.ts +20 -0
  31. package/vendor/sdk/lib/orb/presets.ts +104 -0
  32. package/vendor/sdk/lib/orb/theme.ts +83 -0
  33. package/vendor/sdk/lib/orb/types.ts +73 -0
  34. package/vendor/sdk/lib/primitives.tsx +114 -5
  35. package/vendor/sdk/lib/render.tsx +25 -5
  36. package/vendor/sdk/lib/style.ts +6 -2
  37. package/vendor/sdk/lib/template.tsx +77 -218
  38. package/vendor/sdk/main.tsx +7 -0
  39. package/vendor/skills/unoverse-create/SKILL.md +18 -18
  40. package/vendor/skills/unoverse-create/references/agent-skill.md +12 -12
  41. package/vendor/skills/unoverse-create/references/block.md +5 -5
  42. package/vendor/skills/unoverse-create/references/component.md +150 -75
  43. package/vendor/skills/unoverse-create/references/node.md +1 -1
  44. package/vendor/skills/unoverse-create/references/template.md +142 -82
  45. package/vendor/skills/unoverse-create/references/workflow.md +15 -15
@@ -1,115 +1,175 @@
1
- # Playbook — Templates (MCP apps)
1
+ # Playbook: Templates (MCP apps)
2
2
 
3
3
  **Read first:** `docs/design/templates.md` (layouts + manifest-as-envelope) +
4
- `docs/design/state.md` (the reaction contract + name-sync). Deep law:
5
- `UNOVERSE_STATE_MODEL.md` §5b, `UNOVERSE_LAYERS.md`. Everything in the component
6
- playbook (closed vocabulary, tokens, scale steps) applies here too.
7
-
8
- ## THE model — layouts sync by name
9
-
10
- A component is a Switch of **faces**; a template is a set of **layouts** — same
11
- mechanism, two tiers, connected by ONE rule:
12
-
13
- > A component enters a view → the template presents the layout of the same name.
14
- > No matching layout → the default layout; the component renders inline.
15
-
16
- Derived off the store, never stored, no wiring — the component never knows templates
17
- exist. **Rule of thumb: component causes it = layout; template knows it = state.**
4
+ `docs/design/state.md` (the state model). Deep law: `UNOVERSE_STATE_MODEL.md` §5
5
+ (the six rules), `UNOVERSE_LAYERS.md`. Everything in the component playbook (closed
6
+ vocabulary, tokens, scale steps) applies here too. **Study the exemplar before
7
+ writing**: `rx/sab/templates/sab-chat-layout` is the canonical State Model v2 template.
8
+
9
+ ## THE model: the component drives; the template reacts by name
10
+
11
+ A component is a state tree whose public states are its whole interface; a template
12
+ declares its OWN state tree in the manifest and reacts to components by name:
13
+
14
+ > A component enters a public view → the template asks "do I have a state with that
15
+ > name?" Match: the template enters that state and its layout draws. No match: the
16
+ > component renders inline in the flow. Always.
17
+
18
+ Derived off the store, never stored, no wiring. The component never knows templates
19
+ exist, and post-spawn the template NEVER writes a component's state. Name-match is the
20
+ default convention (`reactsTo:` on a template state is the rare escape hatch for
21
+ vocabulary mismatches). **Rule of thumb: component causes it = a reaction state;
22
+ template knows it = a base substate or template state.**
23
+
24
+ ## The manifest states tree: one declaration, everything follows
25
+
26
+ The canonical shape (`rx/sab/templates/sab-chat-layout/manifest.yaml`):
27
+
28
+ ```yaml
29
+ states:
30
+ main: # the base arrangement, FIRST
31
+ states:
32
+ welcome: {} # contained substate; first substate = the landing default
33
+ focus: {} # then the reaction ladder, in priority order
34
+ products: {}
35
+ detail: {}
36
+ ```
18
37
 
19
- ## The anatomy — manifest-only (NO `<name>.json` envelope)
38
+ - **Top-level order IS the priority ladder**, base first. The template can be in ONE
39
+ state at a time: it walks the list top-down and enters the first of its states that
40
+ ANY hosted component currently matches. The ladder is DERIVED from the tree; never
41
+ author `stateOrder` (that field survives only as the legacy form for templates
42
+ without a tree).
43
+ - **Nesting IS containment, enforced by compilation.** A declared base substate
44
+ (`welcome`) is stripped from every non-base arrangement; no hand-written guard ever
45
+ polices it. The substate's file keeps its own condition (`visibleWhen: isEmpty`).
46
+ - **The shell rule.** Base content drawn in every arrangement is NOT declared as a
47
+ substate. sab's conversation timeline is main's own shell, included by the shared
48
+ chat chrome and legitimately drawn beside the rail, the page and under focus;
49
+ declaring it would contain it to `main` and empty the chat column in every reaction
50
+ state. Only states the template must scope get declared.
51
+ - **Arrival honors the component's declared initial** when the template has a matching
52
+ state; otherwise the template scans its own states in declared order and takes the
53
+ first name on the component's public menu; no overlap and the component rides inline.
54
+ - **A higher state CANCELS the claims below it**: while active, hosted components in
55
+ lower-ranked states retract to inline (durable `lifetime: "conversation"` slices opt
56
+ out), so releasing it lands on the BASE arrangement, never a stale lower one.
57
+ - Ties (two components in the same view): most recent write wins (`limit: 1`).
58
+ - **No word is special.** `focus` outranks `products` by list position only; the engine
59
+ matches names and walks the list. Even priority is data.
60
+ - The template's own condition-guarded moods (a welcome hero on an empty conversation)
61
+ are private substates of the base, never ladder entries: the template does not
62
+ rearrange between them.
63
+
64
+ ## The anatomy: manifest-only (NO `<name>.yaml` envelope)
20
65
 
21
66
  ```
22
- apps/unoverse/rx/orgs/<org>/templates/<name>/
23
- manifest.json # THE ENVELOPE — the whole app contract (below)
24
- layouts/ # the ARRANGEMENTS — one per component view + the default
25
- standard.json # the default (manifest.layout): the core alone
26
- products.json # view "products" → core + cards rail
27
- product.json # view "product" → core + detail panel
67
+ apps/unoverse/rx/<project>/templates/<name>/
68
+ manifest.yaml # THE ENVELOPE: the whole app contract, incl. the states tree
69
+ layouts/ # the ARRANGEMENTS: one per state that owns one (same-name default)
70
+ main.yaml # the base: the core alone
71
+ products.yaml # state "products" → core + cards rail
72
+ detail.yaml # state "detail" → core + full record page
28
73
  components/ # template-local partials (core, header, composer-bar, turns…)
29
- states/ # LOCAL states only (welcome, conversation, call phases) —
30
- # available in exactly the layouts whose trees include them
74
+ states/ # base substates' drawings (welcome): included plainly by the
75
+ # chrome, own condition inside, containment compiled from the tree
31
76
  ```
32
77
 
33
- Each layout = a complete arrangement: `{ "$include": "components/core" }` + that view's
34
- surface. Shared chrome lives ONCE in `components/`; every layout includes it. A layout
35
- named after a view MUST contain a surface selecting that view (guard-enforced) — the
36
- name claims the instance, the surface renders it.
78
+ Each layout = a complete arrangement: `$include: components/core` + that state's
79
+ surface. Shared chrome lives ONCE in `components/`; every layout includes it. The tree
80
+ claims the instance, the layout's surface renders it: a reaction state's layout holds a
81
+ ComponentSlot selecting the matching view.
37
82
 
38
- ## manifest.json (the app contract)
83
+ ## manifest.yaml (the app contract)
39
84
 
40
- Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped — the
41
- USER's words; findIntent ranks on it — the OPENING words carry the ranking; disqualify by
85
+ Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped: the
86
+ USER's words; findIntent ranks on it, the OPENING words carry the ranking; disqualify by
42
87
  property, never by naming siblings; full contract: docs/design/05 §Naming) ·
43
- `category` · `defaultState` (`"template"` = the full surface) · `inputSchema` ·
44
- `layout` (the DEFAULT layout name) · `stateOrder` (local states + layouts, picker order) ·
45
- `binding` (`workflow` + `trigger` — the app OWNS its workflow; without a real binding it
46
- is not done). Optional: `preview` (the per-LAYOUT mock map for Studio —
47
- `{ "products": ["productcard", "productcard"] }`; repeated name = several instances),
48
- `autoTrigger`, `expose`, `service` (`"voice"` — the channel instantiates the native
49
- service, which projects `callState` into scope).
88
+ `category` · `defaultState: "template"` (the app-level LOAD state, a protocol field,
89
+ nothing to do with component views) · `inputSchema` · `layout` (the BASE layout name) ·
90
+ `states` (the tree above) · `binding` (`workflow` + `trigger`: the app OWNS its
91
+ workflow; without a real binding it is not done). Optional: `preview` (the per-state
92
+ mock map for Studio: `{ "products": ["product-card", "product-card"] }`; repeated
93
+ name = several instances), `autoTrigger`, `expose`, `service` (`"voice"`: the channel
94
+ instantiates the native service, which projects `callState` into scope).
50
95
 
51
- **The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs —
52
- it owns general help and cedes specific jobs by property, naming none.
96
+ **The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs.
97
+ It owns general help and cedes specific jobs by property, naming none.
53
98
 
54
- ## Sizing — each layout owns its widths
99
+ ## Sizing: each layout owns its widths
55
100
 
56
101
  The app = the ACTIVE layout's total, nothing else ever. Inside a layout: the core panel
57
102
  is always open (`appWidth` on the core box), the surface panel declares its width on the
58
103
  ComponentSlot. Lint-enforced:
59
- - `appWidth` = a NAMED size from the org's `styles/semantic/app-sizes.json`
60
- (`chat` · `chat-slim` · `rail` · `panel`) — raw CSS is an ERROR.
61
- - ONE declaration per panel — the frame never declares width/flex.
104
+ - `appWidth` = a NAMED size from the project's `styles/semantic/app-sizes`
105
+ (`chat` · `chat-slim` · `rail` · `panel` · `flex`). Raw CSS is an ERROR.
106
+ - ONE declaration per panel; the frame never declares width/flex.
62
107
  - NEVER on a layout root; NEVER `visibleWhen`-guarded (a conditional arrangement is a
63
- LAYOUT, selected by name).
64
- - An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO layout
65
- of its own — reach for a layout when the ARRANGEMENT changes.
66
- - Nothing in the manifest sizes the app (`width`/`focusWidth` dead — lint error).
108
+ STATE with its own layout).
109
+ - An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO state
110
+ of its own. Reach for a state when the ARRANGEMENT changes.
111
+ - Nothing in the manifest sizes the app (`width`/`focusWidth` dead, a lint error).
67
112
  - Layout root: `overflow: hidden`.
68
113
 
69
- ## Surfaces — templates REACT, they never own
70
-
71
- One instance → one placeholder: the flow is the placeholder for `inline`; a surface (or
72
- a layout, via its inner surface) is the placeholder for a named view. The SDK keeps a
73
- claimed instance out of the flow — never hide a flow copy yourself.
74
-
75
- - **Flow slot**: `{ "type": "ComponentSlot", "select": {} }` — always present.
76
- - **Reaction surface**: select by the VIEW, never by type/id:
77
- ```jsonc
78
- { "type": "ComponentSlot", "appWidth": "panel",
79
- "select": { "from": "all", "where": { "field": "defaultState", "eq": "product" }, "limit": 1 },
80
- "frame": { /* chrome; an inner bare ComponentSlot marks the placement */ } }
114
+ ## Surfaces: templates REACT, they never own
115
+
116
+ One instance → one placeholder: the flow is the placeholder for `inline`; a reaction
117
+ state's layout (via its inner surface) is the placeholder for a matched view. The SDK
118
+ keeps a claimed instance out of the flow; never hide a flow copy yourself.
119
+
120
+ - **Flow slot**: `{ type: ComponentSlot, select: {} }`, always present.
121
+ - **Reaction surface**: select by the public `view`, never by type/id:
122
+ ```yaml
123
+ type: ComponentSlot
124
+ appWidth: panel
125
+ select:
126
+ from: all
127
+ where: { field: view, eq: detail }
128
+ limit: 1
129
+ frame: # chrome; an inner bare ComponentSlot marks the placement
81
130
  ```
82
- ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `defaultState`, ❌ template chrome
83
- writing a component's slice — all lint-flagged.
131
+ ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `view`, ❌ template chrome
132
+ writing a component's slice, all lint-flagged. (`defaultState` as the select field
133
+ is the v1 legacy spelling.)
134
+ - **Input tools are template chrome** (unchanged rule): a composer, an edit form, a
135
+ picker is owned by the template, never a streamed component state; chrome never
136
+ "arrives". A card's Edit stages its data into the template's one input tool.
84
137
  - **Template state** (`setTemplateValue`) is ONLY for the template's own chrome (a
85
- disclosure, the composer `draft`). "Is something focused" is DERIVED — never stored.
138
+ disclosure, the composer `draft`). "Is something focused" is DERIVED, never stored.
86
139
  - **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
87
- inline): `visibleWhen { "field": "surfacedView", "in": ["", "products"] }`.
88
- - **A `limit: 1` surface's occupant FILLS it** automatically — never sprinkle
89
- `minHeight: full` through faces. Rails (no limit) keep content-sized cards.
140
+ inline): `visibleWhen { field: surfacedView, in: ["", "products"] }`.
141
+ - **A `limit: 1` surface's occupant FILLS it** automatically; never sprinkle
142
+ `minHeight: full` through component layouts. Rails (no limit) keep content-sized cards.
90
143
  - A full layer caps its height and scrolls its BODY only (`flex: 1` + `minHeight: 0` +
91
144
  `overflow: auto`; header/footer pinned).
145
+ - `pinned: true` on a reaction layout keeps late-arriving components from yanking the
146
+ page from under a reading guest; the guest's own tap still moves it.
92
147
 
93
148
  ## Voice templates
94
149
 
95
- `"service": "voice"` in the manifest. Call phases (`states/idle…user-speaking`) are
96
- LOCAL states included by the cores; typically a wide core in the default layout and a
97
- slim core (`chat-slim`) in the card layouts. Cards during a call select layouts by name
98
- exactly as in chat. Audio is never wired in a definition.
150
+ `"service": "voice"` in the manifest. Call phases (idle…user-speaking) branch off the
151
+ projected `callState` discriminant inside the base; typically a wide core in the base
152
+ layout and a slim core (`chat-slim`) in the card layouts. Cards during a call match
153
+ template states by name exactly as in chat. Audio is never wired in a definition.
99
154
 
100
- ## New org?
155
+ ## New project?
101
156
 
102
- `rx/orgs/<org>/` = `templates/` + optional `components/` (org-private microapps, names
103
- unique across tiers) + a **complete self-contained** `styles/` (copy
104
- `rx/orgs/default/styles` and re-value the base scales). Addresses: apps
105
- `unoverse://apps/<org>/<name>`, org components `unoverse://components/<org>/<name>`,
106
- themes `unoverse://theme/<org>/<theme>`.
157
+ `rx/<project>/` = `templates/` + optional `components/` (project-private microapps,
158
+ names unique across tiers) + a **complete self-contained** `styles/` (copy an existing
159
+ project's `styles/` and re-value the base scales). Addresses: apps
160
+ `unoverse://apps/<project>/<name>`, project components
161
+ `unoverse://components/<project>/<name>`, themes `unoverse://theme/<project>/<theme>`.
107
162
 
108
163
  ## Validate & ship
109
164
 
110
- 1. Publish from Studio — its lint must report 0 errors (layout/name-sync rules, widths, reaction rules, tokens).
111
- 2. Restart unoverse — the node re-synthesizes.
112
- 3. **See it**: Studio — layout pills on top (default first, stateOrder order), local
113
- states down the left, freely combinable; acting inside the preview transitions like
114
- the runtime. Then run it live: the bound workflow streams components, and entering a
115
- view swaps the layout end-to-end.
165
+ 1. Manifest declares the states tree: base first, reaction states in priority order,
166
+ base-only moods nested inside the base, shell content NOT declared. No authored
167
+ `stateOrder`, no hand-written containment guards.
168
+ 2. Publish from Studio; its lint must report 0 errors (the rules are v2-aware:
169
+ state/layout ownership, widths, reaction rules, tokens).
170
+ 3. Restart unoverse; the node re-synthesizes.
171
+ 4. **See it**: in Studio, the state switcher reads the tree (base first, ladder order),
172
+ base substates beneath, freely combinable; acting inside the preview transitions
173
+ like the runtime. Then run it live: the bound workflow streams components, a card
174
+ entering a public view walks the ladder and rearranges the template end-to-end,
175
+ and its ✕ hands the lower state back.
@@ -1,6 +1,6 @@
1
1
  # Playbook: Build a workflow (via the builder MCP)
2
2
 
3
- Workflows are NOT files in the carve-out — they live in the platform and are built
3
+ Workflows are NOT files in the carve-out. They live in the platform and are built
4
4
  through the **builder MCP** (`canvas`, auto-registered by this repo's
5
5
  `.mcp.json`). You drive the platform's own authoring tools; the canvas updates live
6
6
  in the developer's browser as you build.
@@ -8,15 +8,15 @@ in the developer's browser as you build.
8
8
  ## Prerequisites (check before starting)
9
9
 
10
10
  1. **The platform is running** (`unoverse start`, or `npm run dev` in the platform
11
- repo). The builder listens on `localhost:4106` — local machine only, by design.
11
+ repo). The builder listens on `localhost:4106`, local machine only, by design.
12
12
  2. **The MCP is connected**: `canvas` should show connected with 14 tools.
13
- If it shows failed, the platform wasn't up when the session started — reconnect
13
+ If it shows failed, the platform wasn't up when the session started. Reconnect
14
14
  via `/mcp` after starting it.
15
15
  3. **The developer gives you a pairing code.** Ask them to open a **new empty
16
16
  workflow in Canvas** (http://localhost:3001), click **"Connect agent"** in the
17
17
  toolbar, and paste the short code it shows (e.g. `FW9D-6QKA`). The code binds you
18
- to that one workflow — single use, expires in 10 minutes. There is deliberately no
19
- way to list or discover workflows from here — the code must come from the developer.
18
+ to that one workflow: single use, expires in 10 minutes. There is deliberately no
19
+ way to list or discover workflows from here. The code must come from the developer.
20
20
  Prefer a fresh workflow: saves are provenance-scoped, so wiring into nodes you
21
21
  didn't create may be rejected.
22
22
 
@@ -25,19 +25,19 @@ in the developer's browser as you build.
25
25
  1. **`bindWorkflow` first.** Every other tool refuses until the session is bound,
26
26
  and every call after is pinned server-side to that one canvas.
27
27
  2. **Read the in-band guides before building.** The deep how-to is served by the
28
- platform itself via `readBuilderSkill` — always current, never duplicated here:
29
- - `solution-design` — read FIRST on any new goal
30
- - `workflow-building` — the build protocol + response-field semantics
31
- - `workflow-testing` — runTest / stepNode / reading traces
32
- - `template-references` — the `{{...}}` grammar + fixing lint errors
28
+ platform itself via `readBuilderSkill`, always current, never duplicated here:
29
+ - `solution-design`: read FIRST on any new goal
30
+ - `workflow-building`: the build protocol + response-field semantics
31
+ - `workflow-testing`: runTest / stepNode / reading traces
32
+ - `template-references`: the `{{...}}` grammar + fixing lint errors
33
33
  3. **Build ONE stage at a time.** The loop is: `saveWorkflow` one stage → `runTest`
34
34
  → read the trace → add the next stage based on that evidence. Never design or
35
- pre-wire the whole graph up front — you learn a node's real output fields by
35
+ pre-wire the whole graph up front. You learn a node's real output fields by
36
36
  running it, not by planning.
37
37
  4. **Ground truth rules:** `getCanvas` is the real graph; a nodeType is valid only
38
38
  if `getNodeCatalog` returned it (search it fresh per step, with a one-line
39
39
  `task`); layout and node ids are not yours to set.
40
- 5. **The developer judges the result — never self-certify.** When the workflow runs
40
+ 5. **The developer judges the result. Never self-certify.** When the workflow runs
41
41
  end-to-end with real, non-empty output, report what you built and what the test
42
42
  showed. They watch the canvas live and accept, or ask for changes (which continue
43
43
  on the same bound canvas).
@@ -47,6 +47,6 @@ in the developer's browser as you build.
47
47
  | Symptom | Cause / fix |
48
48
  |---|---|
49
49
  | Every tool returns "No workflow bound" | Call `bindWorkflow` with the pairing code from Canvas "Connect agent" |
50
- | Bind fails "invalid or expired pairing code" | Codes are single-use + 10-min TTL — ask the developer to click "Connect agent" again for a fresh one |
51
- | Tool calls error after working earlier | The platform restarted mid-session — `/mcp` → reconnect |
52
- | saveWorkflow rejects an edge into an existing node | Provenance guard: that node wasn't built this session — build on a fresh workflow, or recreate the stage yourself |
50
+ | Bind fails "invalid or expired pairing code" | Codes are single-use + 10-min TTL: ask the developer to click "Connect agent" again for a fresh one |
51
+ | Tool calls error after working earlier | The platform restarted mid-session: `/mcp` → reconnect |
52
+ | saveWorkflow rejects an edge into an existing node | Provenance guard: that node wasn't built this session. Build on a fresh workflow, or recreate the stage yourself |