@unoverse-platform/studio 0.1.68 → 0.1.70

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 (52) hide show
  1. package/bin/studio.mjs +1 -1
  2. package/local/catalog.ts +2 -2
  3. package/local/mcp.ts +3 -2
  4. package/local/plugin.ts +13 -11
  5. package/local/sample-app/chat/components/chat-region.yaml +10 -0
  6. package/local/sample-app/chat/components/core-focus.yaml +18 -0
  7. package/local/sample-app/chat/components/core.yaml +14 -0
  8. package/local/{sample-template → sample-app}/chat/manifest.yaml +10 -15
  9. package/local/{sample-template/chat → sample-component/text-chat}/components/suggestions.yaml +3 -3
  10. package/local/{sample-template/chat/states/conversation.yaml → sample-component/text-chat/layouts/chat-active.yaml} +0 -1
  11. package/local/{sample-template/chat/states/welcome.yaml → sample-component/text-chat/layouts/chat-welcome.yaml} +0 -1
  12. package/local/sample-component/text-chat/layouts/chat.yaml +77 -0
  13. package/local/sample-component/text-chat/manifest.yaml +10 -0
  14. package/local/sample-component/text-chat/text-chat.yaml +59 -0
  15. package/local/scaffold.mjs +14 -9
  16. package/package.json +2 -1
  17. package/screens/ComponentsView.tsx +8 -1
  18. package/screens/NativeAppHost.tsx +5 -5
  19. package/screens/TemplatesView.tsx +77 -77
  20. package/screens/chrome.ts +1 -1
  21. package/screens/states.ts +3 -3
  22. package/src/App.tsx +20 -9
  23. package/src/NewProject.tsx +1 -1
  24. package/src/host.ts +2 -2
  25. package/vendor/sdk/appEngine.tsx +19 -19
  26. package/vendor/sdk/lib/{template.tsx → app.tsx} +11 -11
  27. package/vendor/sdk/lib/connection.tsx +10 -10
  28. package/vendor/sdk/lib/core/actions.ts +3 -3
  29. package/vendor/sdk/lib/core/{templates.ts → apps.ts} +1 -1
  30. package/vendor/sdk/lib/core/connection.ts +17 -15
  31. package/vendor/sdk/lib/core/index.ts +2 -2
  32. package/vendor/sdk/lib/core/store.ts +22 -22
  33. package/vendor/sdk/lib/core/types.ts +2 -2
  34. package/vendor/sdk/lib/index.ts +3 -3
  35. package/vendor/sdk/lib/render.tsx +1 -1
  36. package/vendor/sdk/lib/voice.tsx +2 -2
  37. package/vendor/sdk/main.tsx +7 -7
  38. package/vendor/skills/unoverse-create/SKILL.md +9 -9
  39. package/vendor/skills/unoverse-create/references/{template.md → app.md} +34 -34
  40. package/vendor/skills/unoverse-create/references/brand.md +8 -8
  41. package/vendor/skills/unoverse-create/references/component.md +39 -19
  42. package/vendor/skills/unoverse-create/references/node.md +2 -2
  43. package/vendor/skills/unoverse-prompt-component/SKILL.md +2 -2
  44. package/local/sample-template/chat/components/core-focus.yaml +0 -46
  45. package/local/sample-template/chat/components/core.yaml +0 -41
  46. /package/local/{sample-template → sample-app}/chat/components/focus-overlay.yaml +0 -0
  47. /package/local/{sample-template → sample-app}/chat/layouts/focused.yaml +0 -0
  48. /package/local/{sample-template → sample-app}/chat/layouts/standard.yaml +0 -0
  49. /package/local/{sample-template/chat → sample-component/text-chat}/components/assistant-turn.yaml +0 -0
  50. /package/local/{sample-template/chat → sample-component/text-chat}/components/header.yaml +0 -0
  51. /package/local/{sample-template/chat → sample-component/text-chat}/components/thinking-dots.yaml +0 -0
  52. /package/local/{sample-template/chat → sample-component/text-chat}/components/user-turn.yaml +0 -0
@@ -27,7 +27,7 @@ function render() {
27
27
  <AppEngine
28
28
  serverUrl={current.serverUrl}
29
29
  apiUrl={current.apiUrl}
30
- templateId={current.templateId}
30
+ appId={current.appId}
31
31
  userId={current.userId}
32
32
  conversationId={current.conversationId}
33
33
  // ANALYTICS (UNOVERSE_ANALYTICS.md). Host-supplied: the same bundle boots inside a
@@ -41,7 +41,7 @@ function render() {
41
41
  // tokenless boot made the SDK skip its guest-id substitution (connection.tsx), so
42
42
  // the session registered a non-guest id and the public-entry gate refused it.
43
43
  // No token ⇒ no accessor ⇒ the SDK mints/persists `guest-<uuid>` per the contract
44
- // (UNOVERSE_MCP_TEMPLATE_PROTOCOL §5, AUTH_TOKEN_FLOW Run Authorization).
44
+ // (UNOVERSE_MCP_APP_PROTOCOL §5, AUTH_TOKEN_FLOW Run Authorization).
45
45
  getAccessToken={current.token ? readToken : undefined}
46
46
  />,
47
47
  );
@@ -82,21 +82,21 @@ connectMcpAppHost((r) => {
82
82
  // hardcode/guess it — a baked URL ties the bundle to one environment and hides a
83
83
  // missing config. If it's absent, fail loudly and don't mount.
84
84
  const serverUrl = (meta["unoverse/serverUrl"] as string) || (out.serverUrl as string);
85
- const templateId = (out.app as string) || (meta["unoverse/app"] as string);
85
+ const appId = (out.app as string) || (meta["unoverse/app"] as string);
86
86
  const conversationId = (out.conversationId as string) || (meta["unoverse/conversationId"] as string);
87
87
  const token = (meta["unoverse/token"] as string) || undefined;
88
88
  const userId = (meta["unoverse/userId"] as string) || undefined;
89
- if (!templateId || !serverUrl) {
89
+ if (!appId || !serverUrl) {
90
90
  // eslint-disable-next-line no-console
91
91
  console.warn("[unoverse:boot] cannot mount — tool result carried no serverUrl/app", {
92
92
  serverUrl,
93
- templateId,
93
+ appId,
94
94
  structuredContent: out,
95
95
  metaKeys: Object.keys(meta),
96
96
  });
97
97
  return;
98
98
  }
99
99
  // eslint-disable-next-line no-console
100
- console.log("[unoverse:boot] mounting from MCP host", { serverUrl, templateId, conversationId, userId, hasToken: Boolean(token) });
101
- apply({ serverUrl, templateId, conversationId, userId, token });
100
+ console.log("[unoverse:boot] mounting from MCP host", { serverUrl, appId, conversationId, userId, hasToken: Boolean(token) });
101
+ apply({ serverUrl, appId, conversationId, userId, token });
102
102
  });
@@ -1,6 +1,6 @@
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 (design/), brand and org packs (themes, tokens, rebranding, cloning a universe for a new client), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use whenever the user wants to create, add, build, or modify a component, template, card, page, theme, brand, org pack, agent skill, prompt block, custom node, or workflow in an Unoverse project, even if they don't say 'Unoverse' (they'll describe UI like 'a card with a button and an image' or 'a page the AI fills in')."
3
+ description: "Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), apps, atoms and styles (design/), brand and org packs (themes, tokens, rebranding, cloning a universe for a new client), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use whenever the user wants to create, add, build, or modify a component, app, card, page, theme, brand, org pack, agent skill, prompt block, custom node, or workflow in an Unoverse project, even if they don't say 'Unoverse' (they'll describe UI like 'a card with a button and an image' or 'a page the AI fills in')."
4
4
  ---
5
5
 
6
6
  # Creating Unoverse Artifacts
@@ -10,7 +10,7 @@ the platform reads.
10
10
 
11
11
  | Folder | What lives there |
12
12
  |---|---|
13
- | `design/` | **Design** (data, not code): per-project components, templates + styles; each project is `design/<project>/` |
13
+ | `design/` | **Design** (data, not code): per-project components, apps + styles; each project is `design/<project>/` |
14
14
  | `prompts/` | **Behavior**: agent skills (`skills/`) + prompt blocks (`blocks/`) |
15
15
  | `nodes/` | **Logic**: custom workflow nodes, authored as YAML |
16
16
 
@@ -41,7 +41,7 @@ anything**:
41
41
  | The user wants… | Read |
42
42
  |---|---|
43
43
  | A UI component or shared atom, incl. a page/card the AI fills with content (briefed) | `references/component.md` (§Briefs for AI-filled) |
44
- | A template / app layout (chat surface, wizard shell) | `references/template.md` |
44
+ | An app (chat surface, wizard shell) | `references/app.md` |
45
45
  | A style / theme / token change on ONE component | `references/component.md` §Tokens |
46
46
  | A brand: an org pack, a rebrand, a new client universe, cloning an org | `references/brand.md` |
47
47
  | An agent skill (behavior guide the AI follows) | `references/agent-skill.md` |
@@ -61,12 +61,12 @@ If it's ambiguous (e.g. "add a card that shows weather"), it's usually a **compo
61
61
  ([quick-start](https://docs.unoverse.ai/design/quick-start.md) →
62
62
  [components](https://docs.unoverse.ai/design/components.md) →
63
63
  [state](https://docs.unoverse.ai/design/state.md) →
64
- [templates](https://docs.unoverse.ai/design/templates.md) →
64
+ [apps](https://docs.unoverse.ai/design/apps.md) →
65
65
  [styles-and-tokens](https://docs.unoverse.ai/design/styles-and-tokens.md) →
66
66
  [studio](https://docs.unoverse.ai/design/studio.md) →
67
67
  [validate-and-ship](https://docs.unoverse.ai/design/validate-and-ship.md) →
68
68
  [troubleshooting](https://docs.unoverse.ai/design/troubleshooting.md)) teaches
69
- components, templates, the state model (incl. locked voice/native state), tokens,
69
+ components, apps, the state model (incl. locked voice/native state), tokens,
70
70
  Studio, and validation in order;
71
71
  [design/CLAUDE.md](https://docs.unoverse.ai/design/CLAUDE.md) is the condensed agent
72
72
  rulebook. The playbooks here assume that material. Append `.md` to any docs URL to
@@ -87,11 +87,11 @@ fetch it as raw markdown.
87
87
  3. **UI is data.** No pixels, hex colors, or CSS anywhere: token names only. No logic
88
88
  in definitions: anything computed (totals, formatting, chosen colors) is computed in
89
89
  the node and sent as a plain field.
90
- 4. **Audit before you're done.** Every component/template must pass the conformance
90
+ 4. **Audit before you're done.** Every component/app must pass the conformance
91
91
  checklist ([UNOVERSE_AUTHORING](https://docs.unoverse.ai/unoverse/UNOVERSE_AUTHORING.md) §9).
92
92
  Walk it item by item.
93
93
  5. **Discovery meta is ranked, not read.** Any spatially-discoverable artifact (a
94
- component or template with a manifest, an agent skill, a node) is selected by
94
+ component or app with a manifest, an agent skill, a node) is selected by
95
95
  embedding `title`/`name` + `whenToUse`/`description` + `category` against the user's
96
96
  own words (or the planner's task). The strict contract is
97
97
  [node-discoverability](https://docs.unoverse.ai/nodes/node-discoverability.md):
@@ -101,7 +101,7 @@ fetch it as raw markdown.
101
101
 
102
102
  ## Step 3: Deploy loop (after the artifact is written)
103
103
 
104
- Components and templates have NO scaffold command. Author them from the references
104
+ Components and apps have NO scaffold command. Author them from the references
105
105
  (mirror the closest existing artifact). Validation lives in the deploy lint (`unoverse deploy studio`): it
106
106
  lints every definition before anything leaves the machine (0 errors required, doc-cited
107
107
  messages; justify any warning), and the conformance checklist is
@@ -109,7 +109,7 @@ messages; justify any warning), and the conformance checklist is
109
109
 
110
110
  | Artifact | To see it live |
111
111
  |---|---|
112
- | Component / atom / template / style | Preview in Studio (`unoverse studio`), then `unoverse deploy studio`: the deploy lint gates it |
112
+ | Component / atom / app / style | Preview in Studio (`unoverse studio`), then `unoverse deploy studio`: the deploy lint gates it |
113
113
  | Existing component restyle/edit only | takes effect live (the SDK reads `design/` directly) |
114
114
  | Agent skill / prompt block | `unoverse deploy studio`, same gate |
115
115
  | Node | Studio's Nodes screen: Load sample → Run. A node is YAML; running it is the only proof (no build) |
@@ -1,30 +1,30 @@
1
- # Playbook: Templates (MCP apps)
1
+ # Playbook: Apps
2
2
 
3
- **Read first:** [design/templates](https://docs.unoverse.ai/design/templates.md)
3
+ **Read first:** [design/apps](https://docs.unoverse.ai/design/apps.md)
4
4
  (layouts + manifest-as-envelope) +
5
5
  [design/state](https://docs.unoverse.ai/design/state.md) (the state model). Deep law:
6
6
  [UNOVERSE_STATE_MODEL](https://docs.unoverse.ai/unoverse/UNOVERSE_STATE_MODEL.md) §5
7
7
  (the six rules), [UNOVERSE_LAYERS](https://docs.unoverse.ai/unoverse/UNOVERSE_LAYERS.md).
8
8
  Everything in the component playbook (closed vocabulary, tokens, scale steps) applies
9
- here too. **Study an exemplar before writing**: templates are always a project's own
9
+ here too. **Study an exemplar before writing**: apps are always a project's own
10
10
  (none ship in the base set), so your references are the canonical manifest tree inlined
11
- below, [design/templates](https://docs.unoverse.ai/design/templates.md), and any template
12
- already in the workspace. (Monorepo: `design/sab/templates/sab-chat-layout`.)
11
+ below, [design/apps](https://docs.unoverse.ai/design/apps.md), and any app
12
+ already in the workspace. (Monorepo: `design/sab/apps/sab-chat`.)
13
13
 
14
- ## THE model: the component drives; the template reacts by name
14
+ ## THE model: the component drives; the app reacts by name
15
15
 
16
- A component is a state tree whose public states are its whole interface; a template
16
+ A component is a state tree whose public states are its whole interface; an app
17
17
  declares its OWN state tree in the manifest and reacts to components by name:
18
18
 
19
- > A component enters a public view → the template asks "do I have a state with that
20
- > name?" Match: the template enters that state and its layout draws. No match: the
19
+ > A component enters a public view → the app asks "do I have a state with that
20
+ > name?" Match: the app enters that state and its layout draws. No match: the
21
21
  > component renders inline in the flow. Always.
22
22
 
23
- Derived off the store, never stored, no wiring. The component never knows templates
24
- exist, and post-spawn the template NEVER writes a component's state. Name-match is the
25
- default convention (`reactsTo:` on a template state is the rare escape hatch for
23
+ Derived off the store, never stored, no wiring. The component never knows apps
24
+ exist, and post-spawn the app NEVER writes a component's state. Name-match is the
25
+ default convention (`reactsTo:` on an app state is the rare escape hatch for
26
26
  vocabulary mismatches). **Rule of thumb: component causes it = a reaction state;
27
- template knows it = a base substate or template state.**
27
+ app knows it = a base substate or app state.**
28
28
 
29
29
  ## The manifest states tree: one declaration, everything follows
30
30
 
@@ -40,10 +40,10 @@ states:
40
40
  detail: {}
41
41
  ```
42
42
 
43
- - **Top-level order IS the priority ladder**, base first. The template can be in ONE
43
+ - **Top-level order IS the priority ladder**, base first. The app can be in ONE
44
44
  state at a time: it walks the list top-down and enters the first of its states that
45
45
  ANY hosted component currently matches. The ladder is DERIVED from the tree; never
46
- author `stateOrder` (that field survives only as the legacy form for templates
46
+ author `stateOrder` (that field survives only as the legacy form for apps
47
47
  without a tree).
48
48
  - **Nesting IS containment, enforced by compilation.** A declared base substate
49
49
  (`welcome`) is stripped from every non-base arrangement; no hand-written guard ever
@@ -52,9 +52,9 @@ states:
52
52
  substate. sab's conversation timeline is main's own shell, included by the shared
53
53
  chat chrome and legitimately drawn beside the rail, the page and under focus;
54
54
  declaring it would contain it to `main` and empty the chat column in every reaction
55
- state. Only states the template must scope get declared.
56
- - **Arrival honors the component's declared initial** when the template has a matching
57
- state; otherwise the template scans its own states in declared order and takes the
55
+ state. Only states the app must scope get declared.
56
+ - **Arrival honors the component's declared initial** when the app has a matching
57
+ state; otherwise the app scans its own states in declared order and takes the
58
58
  first name on the component's public menu; no overlap and the component rides inline.
59
59
  - **A higher state CANCELS the claims below it**: while active, hosted components in
60
60
  lower-ranked states retract to inline (durable `lifetime: "conversation"` slices opt
@@ -62,20 +62,20 @@ states:
62
62
  - Ties (two components in the same view): most recent write wins (`limit: 1`).
63
63
  - **No word is special.** `focus` outranks `products` by list position only; the engine
64
64
  matches names and walks the list. Even priority is data.
65
- - The template's own condition-guarded moods (a welcome hero on an empty conversation)
66
- are private substates of the base, never ladder entries: the template does not
65
+ - The app's own condition-guarded moods (a welcome hero on an empty conversation)
66
+ are private substates of the base, never ladder entries: the app does not
67
67
  rearrange between them.
68
68
 
69
69
  ## The anatomy: manifest-only (NO `<name>.yaml` envelope)
70
70
 
71
71
  ```
72
- design/<project>/templates/<name>/
72
+ design/<project>/apps/<name>/
73
73
  manifest.yaml # THE ENVELOPE: the whole app contract, incl. the states tree
74
74
  layouts/ # the ARRANGEMENTS: one per state that owns one (same-name default)
75
75
  main.yaml # the base: the core alone
76
76
  products.yaml # state "products" → core + cards rail
77
77
  detail.yaml # state "detail" → core + full record page
78
- components/ # template-local partials (core, header, composer-bar, turns…)
78
+ components/ # app-local partials (core, header, composer-bar, turns…)
79
79
  states/ # base substates' drawings (welcome): included plainly by the
80
80
  # chrome, own condition inside, containment compiled from the tree
81
81
  ```
@@ -90,8 +90,8 @@ ComponentSlot selecting the matching view.
90
90
  Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped: the
91
91
  USER's words; findIntent ranks on it, the OPENING words carry the ranking; disqualify by
92
92
  property, never by naming siblings; full contract:
93
- [design/templates](https://docs.unoverse.ai/design/templates.md) §Naming) ·
94
- `category` · `defaultState: "template"` (the app-level LOAD state, a protocol field,
93
+ [design/apps](https://docs.unoverse.ai/design/apps.md) §Naming) ·
94
+ `category` · `defaultState: "app"` (the app-level LOAD state, a protocol field,
95
95
  nothing to do with component views) · `inputSchema` · `layout` (the BASE layout name) ·
96
96
  `states` (the tree above) · `binding` (`workflow` + `trigger`: the app OWNS its
97
97
  workflow; without a real binding it is not done). Optional: `preview` (the per-state
@@ -117,7 +117,7 @@ ComponentSlot. Lint-enforced:
117
117
  - Nothing in the manifest sizes the app (`width`/`focusWidth` dead, a lint error).
118
118
  - Layout root: `overflow: hidden`.
119
119
 
120
- ## Surfaces: templates REACT, they never own
120
+ ## Surfaces: apps REACT, they never own
121
121
 
122
122
  One instance → one placeholder: the flow is the placeholder for `inline`; a reaction
123
123
  state's layout (via its inner surface) is the placeholder for a matched view. The SDK
@@ -134,13 +134,13 @@ keeps a claimed instance out of the flow; never hide a flow copy yourself.
134
134
  limit: 1
135
135
  frame: # chrome; an inner bare ComponentSlot marks the placement
136
136
  ```
137
- ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `view`, ❌ template chrome
137
+ ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `view`, ❌ app chrome
138
138
  writing a component's slice, all lint-flagged. (`defaultState` as the select field
139
139
  is the v1 legacy spelling.)
140
- - **Input tools are template chrome** (unchanged rule): a composer, an edit form, a
141
- picker is owned by the template, never a streamed component state; chrome never
142
- "arrives". A card's Edit stages its data into the template's one input tool.
143
- - **Template state** (`setTemplateValue`) is ONLY for the template's own chrome (a
140
+ - **Input tools are app chrome** (unchanged rule): a composer, an edit form, a
141
+ picker is owned by the app, never a streamed component state; chrome never
142
+ "arrives". A card's Edit stages its data into the app's one input tool.
143
+ - **App state** (`setTemplateValue`) is ONLY for the app's own chrome (a
144
144
  disclosure, the composer `draft`). "Is something focused" is DERIVED, never stored.
145
145
  - **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
146
146
  inline): `visibleWhen { field: surfacedView, in: ["", "products"] }`.
@@ -151,16 +151,16 @@ keeps a claimed instance out of the flow; never hide a flow copy yourself.
151
151
  - `pinned: true` on a reaction layout keeps late-arriving components from yanking the
152
152
  page from under a reading guest; the guest's own tap still moves it.
153
153
 
154
- ## Voice templates
154
+ ## Voice apps
155
155
 
156
156
  `"service": "voice"` in the manifest. Call phases (idle…user-speaking) branch off the
157
157
  projected `callState` discriminant inside the base; typically a wide core in the base
158
158
  layout and a slim core (`chat-slim`) in the card layouts. Cards during a call match
159
- template states by name exactly as in chat. Audio is never wired in a definition.
159
+ app states by name exactly as in chat. Audio is never wired in a definition.
160
160
 
161
161
  ## New project?
162
162
 
163
- `design/<project>/` = `templates/` + optional `components/` (project-private microapps;
163
+ `design/<project>/` = `apps/` + optional `components/` (project-private microapps;
164
164
  names unique within the project and never shadowing a marketplace name; two projects
165
165
  may share a name, each addressed `<org>/<name>`) + `styles/` holding ONLY the org's
166
166
  overrides (the foundation cascades per token; the theme file is the one you copy
@@ -179,5 +179,5 @@ whole, see brand.md). Addresses: apps
179
179
  4. **See it**: in Studio, the state switcher reads the tree (base first, ladder order),
180
180
  base substates beneath, freely combinable; acting inside the preview transitions
181
181
  like the runtime. Then run it live: the bound workflow streams components, a card
182
- entering a public view walks the ladder and rearranges the template end-to-end,
182
+ entering a public view walks the ladder and rearranges the app end-to-end,
183
183
  and its ✕ hands the lower state back.
@@ -66,7 +66,7 @@ design/<neworg>/
66
66
  themes/studio.yaml
67
67
  semantic/*.yaml deltas only, omit what matches the foundation
68
68
  components/ org-private components (optional)
69
- templates/ the org's app layouts (optional)
69
+ apps/ the org's app layouts (optional)
70
70
  ```
71
71
 
72
72
  **`base/` and `semantic/` cascade. The theme file does NOT.**
@@ -83,7 +83,7 @@ you want. That is the one place copying is correct, and it is the opposite of th
83
83
  Add `base/color.yaml` once you have brand ingredients to name, and `semantic/` only where
84
84
  you genuinely differ.
85
85
 
86
- Its theme is then served at `unoverse://theme/<neworg>/light`, and a template points at it.
86
+ Its theme is then served at `unoverse://theme/<neworg>/light`, and an app points at it.
87
87
 
88
88
  ## 4. Anatomy of an org pack (`design/<org>/styles/`)
89
89
 
@@ -132,19 +132,19 @@ Copying an org pack is mechanical, and every gotcha is a name that exists in mor
132
132
  place. Work through them in order.
133
133
 
134
134
  1. **Copy the folder.** `design/<source>/` → `design/<neworg>/`, keeping `styles/`, `components/`,
135
- `templates/`.
136
- 2. **Rename the templates.** Folder AND the id inside the manifest. `bpp-chat-layout`
137
- becomes `lyceum-chat-layout` in both places. Template ids are org-qualified and they
135
+ `apps/`.
136
+ 2. **Rename the apps.** Folder AND the id inside the manifest. `bpp-chat`
137
+ becomes `lyceum-chat-layout` in both places. App ids are org-qualified and they
138
138
  are referenced by string.
139
139
  3. **Component names stay as copied.** Names are unique per org, not across orgs, so a
140
140
  cloned pack carrying the same component names as its source is legal: each org's copy
141
141
  is addressed `unoverse://components/<org>/<name>`. The one forbidden case is a name
142
142
  that shadows a design-system (marketplace) component; the publish lint catches it.
143
- Update template manifest `name`s only where the id itself is org-branded.
143
+ Update app manifest `name`s only where the id itself is org-branded.
144
144
  4. **Register the client surface** if a demo client serves it (platform monorepo only:
145
145
  `packages/demo/src/demo/clients.js`).
146
- 5. **Point the app at the new theme.** Templates reference `unoverse://theme/<org>/<name>`.
147
- A copied template still names the old org until you change it.
146
+ 5. **Point the app at the new theme.** Apps reference `unoverse://theme/<org>/<name>`.
147
+ A copied app still names the old org until you change it.
148
148
 
149
149
  ## 7. Done means all four
150
150
 
@@ -33,16 +33,16 @@ A component is **data plus a state tree**; each state owns the layout(s) that dr
33
33
  The root's `state:` block IS the design, readable top to bottom.
34
34
 
35
35
  > **THE GATE — answer this before adding ANY state to `view`:**
36
- > *"Would a hosting template rearrange the page to show this?"*
36
+ > *"Would a hosting app rearrange the page to show this?"*
37
37
  > **No → it's a private step**: nest it under `on:` inside an existing public state.
38
38
  > **Yes → public.**
39
39
  > ⚠ "The user asked for N states" never means N public states. In-place swaps
40
40
  > (confirmation, success, empty, error, loading, a form's next step) are almost always
41
41
  > PRIVATE. Privacy is the default; promotion to `view` is a deliberate act with a
42
- > template consequence.
42
+ > app consequence.
43
43
 
44
44
  The canonical shape (a product-card component). Note where each state landed and why:
45
- `products` and `detail` are PUBLIC because a template genuinely rearranges for each (a
45
+ `products` and `detail` are PUBLIC because an app genuinely rearranges for each (a
46
46
  grid rail vs a full page); `detail`'s inner `detail`/`apply` are PRIVATE steps because
47
47
  applying swaps content in place — the page does not move:
48
48
 
@@ -75,20 +75,22 @@ The rules, in the order they bite:
75
75
  The root is a `Switch on view`; buttons transition with
76
76
  `setValue { values: [{ key: view, value: detail }] }`. That write is the ENTIRE
77
77
  transition machinery: no guards, no events, no per-component machines.
78
- - **Public states are the top level of the `view` axis** and they are the template's
79
- WHOLE vocabulary: everything templates can react to, nothing more. Steps nest
78
+ - **Public states are the top level of the `view` axis** and they are the app's
79
+ WHOLE vocabulary: everything apps can react to, nothing more. Steps nest
80
80
  privately (`on: <key>` + `initial` + `states`), invisible outside the component.
81
- The placement rule is THE GATE above: **if the template must rearrange to show it,
82
- make it public; if the template wouldn't move, nest it privately.** Privacy is the
81
+ The placement rule is THE GATE above: **if the app must rearrange to show it,
82
+ make it public; if the app wouldn't move, nest it privately.** Privacy is the
83
83
  default; promotion is deliberate.
84
84
  **The tell in Studio:** non-initial public states preview FULL-BLEED (no stage
85
- padding), precisely because they are template-liftable surfaces. If a state you
85
+ padding), precisely because they are app-liftable surfaces. If a state you
86
86
  meant as an in-place swap suddenly renders edge-to-edge, you promoted a private
87
87
  step to `view` — move it into the nest.
88
88
  - **Each state owns its layout(s).** `layout:` is OPTIONAL: a state with no declaration
89
89
  draws the layout of its own name, and `{}` is a complete state. Write `layout:` only
90
90
  when the filename differs. A layout is never addressable on its own; only its owning
91
- state selects it. Name states for what they MEAN (`detail`), never for their drawing.
91
+ state selects it. Name states for what they MEAN (`detail`), never for their drawing. **Substate drawings are axis-prefixed**: a step's file carries its axis key
92
+ (`step-register`, `callState-idle`) and is declared via `layout:`; unprefixed
93
+ files are arrangements (exemplars: `course-application`, `voice-chat`).
92
94
  - **Shell vs steps.** A state's own layout is its SHELL, always on while the state is
93
95
  active. Its nested substates are the STEPS, the only choices inside it, addressed by
94
96
  state names (layout filenames are plumbing).
@@ -96,13 +98,13 @@ The rules, in the order they bite:
96
98
  `focused: { layouts: { horizontal: focused-h, vertical: focused-v } }`. Variants bind
97
99
  the SAME fields; anything that changes meaning or bound data is a second state
98
100
  (lint-enforced). The component picks its variant by the space it finds itself in
99
- (container queries); templates influence it only by controlling the container.
100
- - **Arrival: nobody sends a state.** The host template places the spawned actor: it
101
+ (container queries); apps influence it only by controlling the container.
102
+ - **Arrival: nobody sends a state.** The host app places the spawned actor: it
101
103
  honors the component's declared `initial` when it has a matching state; otherwise it
102
104
  scans its own states in declared order and takes the first name on the component's
103
105
  public menu; no overlap and the component wakes in its own initial chain and renders
104
106
  inline. The server compiles the tree to the public menu + initials it serves
105
- (`publicStates`, `initialView`). Post-spawn, the template NEVER writes a component's
107
+ (`publicStates`, `initialView`). Post-spawn, the app NEVER writes a component's
106
108
  state.
107
109
  - **Unmatched view = inline. Always.** An ignored public state does not stay put, it
108
110
  FALLS INLINE in the flow. A step that must never move the page belongs in the nest,
@@ -120,12 +122,30 @@ The rules, in the order they bite:
120
122
  static → hardcode);
121
123
  - workflow-fed data → **`props` with `input: true`** (the `default` is the preview mock).
122
124
  Usually empty.
125
+ - **Projected-field preview rule:** any LOCKED/projected field a
126
+ component branches on (`callState`, `isEmpty`, `hasMessages`) must ALSO be declared as
127
+ a prop with a preview default: Studio's bare component preview supplies ONLY prop
128
+ defaults, so an undeclared projected read renders NOTHING there; live, the projected
129
+ value wins and the default never applies. A Ref-embedded component's root Switch
130
+ likewise needs a `default:` case (`view` is absent without a spawned slice).
131
+ - **Who owns a mood: the DRIVER decides.** Ask what WRITES the discriminant:
132
+ (1) the component's own buttons (`setValue`) → component substates on a private axis,
133
+ names from the design (`course-application`); (2) a service projecting a NAMED value
134
+ (`callState`) → component substates named for those values + a preview prop
135
+ (`voice-chat`); (3) DERIVED from conversation facts (`isEmpty`/`hasMessages`) → the
136
+ APP tier owns it: a condition-guarded mood, or guarded drawings inside an
137
+ embedded component reacting to the same facts — the app has the state,
138
+ components just react. NEVER model a derived mood as component named substates, and
139
+ never borrow another field's values (`lifecycle`) as state names (bpp `text-chat`). Declare the axis as a prop with enum + default
140
+ (preview only; live the projection wins), give the root Switch a `default:` case for
141
+ Ref placement, and prefix the step drawings with the axis (`callState-idle`,
142
+ `lifecycle-active`). Exemplars: `voice-chat`, bpp `text-chat`.
123
143
  - **Prop names = the data contract. Never invent them.** Source data hydrates props
124
144
  BY NAME (as-is, no mapping); a name the source doesn't carry silently renders the
125
145
  preview default (tell: title streams, image stays mock). Content-attached cards use
126
146
  the writer vocabulary: `title` `tagline` `description` `bodyCopy` `introParagraph`
127
147
  `primaryImage` `images` `link` `callToAction`. Law:
128
- `UNOVERSE_MCP_TEMPLATE_PROTOCOL.md` §Content-attached cards (platform spec; a
148
+ `UNOVERSE_MCP_APP_PROTOCOL.md` §Content-attached cards (platform spec; a
129
149
  guard test in the platform server enforces it).
130
150
  2. **Root = `Switch on view`**, cases per PUBLIC state, each `$include` of the state's
131
151
  layout (the tree above). Keep an `inline` state for any component that lives in a
@@ -133,22 +153,22 @@ The rules, in the order they bite:
133
153
  **single-state (permanent) component**: exactly ONE public state, NO inline. It
134
154
  arrives in its surface, cannot leave it (no ✕, no other state), and is enriched in
135
155
  place via data merges to one stable instance, e.g. a continuously-updated page
136
- living in a template's focus state. Permanence is by construction, never by a
156
+ living in an app's focus state. Permanence is by construction, never by a
137
157
  pinning rule.
138
158
  **Lifetime (OPTIONAL manifest flag).** `"lifetime": "conversation"` marks a durable,
139
159
  conversation-scoped surface (a cart, an itinerary, a composed page): the platform keys
140
160
  its instance by the CONVERSATION (every re-call hydrates the SAME slice: merge, not
141
161
  re-place) and the new-turn reset skips it, so it stays on screen until it is replaced,
142
- closes itself, or a NEW TEMPLATE loads (the template swap is the hard refresh boundary).
143
- Durable slices also opt out of `cancelBelow` retraction when a higher template state
162
+ closes itself, or a NEW APP loads (the app swap is the hard refresh boundary).
163
+ Durable slices also opt out of `cancelBelow` retraction when a higher app state
144
164
  takes over. Default `"turn"`: the instance returns to inline / retires on the next
145
165
  user turn. Values are a closed set: `turn | conversation` (lint-checked;
146
166
  [design/components](https://docs.unoverse.ai/design/components.md)).
147
167
  3. **State is local; the `view` is the interface** (STATE_MODEL §5). Private axes
148
- (`step`, `phase`, …) are the component's own business; the template never sees them.
168
+ (`step`, `phase`, …) are the component's own business; the app never sees them.
149
169
  The only thing that crosses is the public `view`. The component writes ONLY its own
150
170
  slice (`setValue`): expand = `setValue { view: "detail" }`; its expanded layout
151
- carries its own ✕ back down. When a hosting template has a state of that name, the
171
+ carries its own ✕ back down. When a hosting app has a state of that name, the
152
172
  instance **lifts out of the flow into that state's slot**. The SDK renders each
153
173
  instance in exactly ONE placeholder, so you never hide a flow copy yourself (no
154
174
  `hideBelow`, no overlay hack). ❌ NEVER `setTemplateValue` to open a surface
@@ -298,6 +318,6 @@ component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
298
318
  the doc for every rule).
299
319
  5. Restart unoverse → Studio: mock (prop defaults + the state switcher reads the tree:
300
320
  public states on top, steps beneath by state name), then live. Sanity-check the
301
- switcher against THE GATE: every entry in its top row is a promise that a template
321
+ switcher against THE GATE: every entry in its top row is a promise that an app
302
322
  will rearrange for it, and a non-initial public state previews full-bleed. Debug:
303
323
  stream log → state inspector → definition; never guess.
@@ -87,7 +87,7 @@ package only for a new integration.
87
87
  `required: false` is right for almost every node: the trigger already decided who was let
88
88
  in. See [who-can-run-it](https://docs.unoverse.ai/nodes/who-can-run-it.md).
89
89
  3. **Write `interface.yaml`.** Name outputs for what they carry, because those names become
90
- what someone types in a template.
90
+ what someone types in an app.
91
91
  4. **Write `config.yaml`.** Every value the workflow supplies needs `ui:field: template`. A
92
92
  `description` says what the setting does, not what it is called. NEVER declare
93
93
  `authRequired` or `authRole`: they are reserved names, injected by the platform into
@@ -126,7 +126,7 @@ the manifest describes the real service rather than something merely coherent.
126
126
 
127
127
  ## Things that go wrong
128
128
 
129
- - **A template resolved to empty.** Nothing errors. The node id or the output name does not
129
+ - **An app resolved to empty.** Nothing errors. The node id or the output name does not
130
130
  match a real edge. There is no `input.*` root, and array indexes are dot segments
131
131
  (`records.0.Name`), never brackets.
132
132
  - **A number arrived as a string.** A field that is exactly one `{{ path }}` keeps its type.
@@ -90,7 +90,7 @@ authored briefs go to die.
90
90
  - type: Each
91
91
  brief: { description: "...", minItems: 4, maxItems: 8 }
92
92
  bind: { items: risks }
93
- template: { $include: components/bullet-list }
93
+ app: { $include: components/bullet-list }
94
94
  ```
95
95
 
96
96
  ### Trap 1: a brief on a `Ref` never compiles
@@ -163,7 +163,7 @@ abbreviate and never substitute characters. Say so in the page-level brief.
163
163
  ## Step 5: the workflow door
164
164
 
165
165
  If the component is filled by an agent inside a workflow rather than in chat, read
166
- `UNOVERSE_MCP_TEMPLATE_PROTOCOL.md` §The workflow door (platform spec; in the monorepo
166
+ `UNOVERSE_MCP_APP_PROTOCOL.md` §The workflow door (platform spec; in the monorepo
167
167
  at `docs/unoverse/`). The load-bearing law:
168
168
  **a blank part is a valid final state.** A part the source does not cover stays blank, the
169
169
  `visibleWhen` guards render around it, and the page is complete. Gaps are orientation on
@@ -1,46 +0,0 @@
1
- # THE CHAT COLUMN WITH A FOCUSED COMPONENT OVER IT — core, plus the focus surface
2
- # laid over the conversation region. A core VARIANT, not a copy: header, states and
3
- # composer are the same shared files core.yaml includes; the only difference is the
4
- # overlay slot.
5
- type: Box
6
- appWidth: chat
7
- style:
8
- direction: column
9
- minHeight: "0"
10
- children:
11
- - type: Box
12
- style:
13
- direction: column
14
- flex: 1
15
- minHeight: "0"
16
- position: relative
17
- children:
18
- - $include: components/header
19
- - type: Box
20
- style:
21
- direction: column
22
- flex: 1
23
- minHeight: "0"
24
- position: relative
25
- when:
26
- - field: hasMessages
27
- eq: true
28
- apply:
29
- background: surface.base
30
- radiusTopLeft: 2xl
31
- overflow: hidden
32
- children:
33
- - $include: states/welcome
34
- - $include: states/conversation
35
- - type: Box
36
- style:
37
- position: absolute
38
- bottom: "0"
39
- left: "0"
40
- right: "0"
41
- direction: column
42
- children:
43
- - $include: components/suggestions
44
- - $include: components/focus-overlay
45
- - type: Ref
46
- ref: composer-bar
@@ -1,41 +0,0 @@
1
- type: Box
2
- appWidth: chat
3
- style:
4
- direction: column
5
- minHeight: "0"
6
- children:
7
- - type: Box
8
- style:
9
- direction: column
10
- flex: 1
11
- minHeight: "0"
12
- position: relative
13
- children:
14
- - $include: components/header
15
- - type: Box
16
- style:
17
- direction: column
18
- flex: 1
19
- minHeight: "0"
20
- position: relative
21
- when:
22
- - field: hasMessages
23
- eq: true
24
- apply:
25
- background: surface.base
26
- radiusTopLeft: 2xl
27
- overflow: hidden
28
- children:
29
- - $include: states/welcome
30
- - $include: states/conversation
31
- - type: Box
32
- style:
33
- position: absolute
34
- bottom: "0"
35
- left: "0"
36
- right: "0"
37
- direction: column
38
- children:
39
- - $include: components/suggestions
40
- - type: Ref
41
- ref: composer-bar