@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.
- package/bin/studio.mjs +1 -1
- package/local/catalog.ts +2 -2
- package/local/mcp.ts +3 -2
- package/local/plugin.ts +13 -11
- package/local/sample-app/chat/components/chat-region.yaml +10 -0
- package/local/sample-app/chat/components/core-focus.yaml +18 -0
- package/local/sample-app/chat/components/core.yaml +14 -0
- package/local/{sample-template → sample-app}/chat/manifest.yaml +10 -15
- package/local/{sample-template/chat → sample-component/text-chat}/components/suggestions.yaml +3 -3
- package/local/{sample-template/chat/states/conversation.yaml → sample-component/text-chat/layouts/chat-active.yaml} +0 -1
- package/local/{sample-template/chat/states/welcome.yaml → sample-component/text-chat/layouts/chat-welcome.yaml} +0 -1
- package/local/sample-component/text-chat/layouts/chat.yaml +77 -0
- package/local/sample-component/text-chat/manifest.yaml +10 -0
- package/local/sample-component/text-chat/text-chat.yaml +59 -0
- package/local/scaffold.mjs +14 -9
- package/package.json +2 -1
- package/screens/ComponentsView.tsx +8 -1
- package/screens/NativeAppHost.tsx +5 -5
- package/screens/TemplatesView.tsx +77 -77
- package/screens/chrome.ts +1 -1
- package/screens/states.ts +3 -3
- package/src/App.tsx +20 -9
- package/src/NewProject.tsx +1 -1
- package/src/host.ts +2 -2
- package/vendor/sdk/appEngine.tsx +19 -19
- package/vendor/sdk/lib/{template.tsx → app.tsx} +11 -11
- package/vendor/sdk/lib/connection.tsx +10 -10
- package/vendor/sdk/lib/core/actions.ts +3 -3
- package/vendor/sdk/lib/core/{templates.ts → apps.ts} +1 -1
- package/vendor/sdk/lib/core/connection.ts +17 -15
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +22 -22
- package/vendor/sdk/lib/core/types.ts +2 -2
- package/vendor/sdk/lib/index.ts +3 -3
- package/vendor/sdk/lib/render.tsx +1 -1
- package/vendor/sdk/lib/voice.tsx +2 -2
- package/vendor/sdk/main.tsx +7 -7
- package/vendor/skills/unoverse-create/SKILL.md +9 -9
- package/vendor/skills/unoverse-create/references/{template.md → app.md} +34 -34
- package/vendor/skills/unoverse-create/references/brand.md +8 -8
- package/vendor/skills/unoverse-create/references/component.md +39 -19
- package/vendor/skills/unoverse-create/references/node.md +2 -2
- package/vendor/skills/unoverse-prompt-component/SKILL.md +2 -2
- package/local/sample-template/chat/components/core-focus.yaml +0 -46
- package/local/sample-template/chat/components/core.yaml +0 -41
- /package/local/{sample-template → sample-app}/chat/components/focus-overlay.yaml +0 -0
- /package/local/{sample-template → sample-app}/chat/layouts/focused.yaml +0 -0
- /package/local/{sample-template → sample-app}/chat/layouts/standard.yaml +0 -0
- /package/local/{sample-template/chat → sample-component/text-chat}/components/assistant-turn.yaml +0 -0
- /package/local/{sample-template/chat → sample-component/text-chat}/components/header.yaml +0 -0
- /package/local/{sample-template/chat → sample-component/text-chat}/components/thinking-dots.yaml +0 -0
- /package/local/{sample-template/chat → sample-component/text-chat}/components/user-turn.yaml +0 -0
package/vendor/sdk/main.tsx
CHANGED
|
@@ -27,7 +27,7 @@ function render() {
|
|
|
27
27
|
<AppEngine
|
|
28
28
|
serverUrl={current.serverUrl}
|
|
29
29
|
apiUrl={current.apiUrl}
|
|
30
|
-
|
|
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
|
-
// (
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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,
|
|
101
|
-
apply({ serverUrl,
|
|
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),
|
|
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,
|
|
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
|
-
|
|
|
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
|
-
[
|
|
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,
|
|
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/
|
|
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
|
|
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
|
|
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 /
|
|
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:
|
|
1
|
+
# Playbook: Apps
|
|
2
2
|
|
|
3
|
-
**Read first:** [design/
|
|
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**:
|
|
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/
|
|
12
|
-
already in the workspace. (Monorepo: `design/sab/
|
|
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
|
|
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;
|
|
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
|
|
20
|
-
> name?" 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
|
|
24
|
-
exist, and post-spawn the
|
|
25
|
-
default convention (`reactsTo:` on
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
56
|
-
- **Arrival honors the component's declared initial** when the
|
|
57
|
-
state; otherwise 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
|
|
66
|
-
are private substates of the base, never ladder entries: the
|
|
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>/
|
|
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/ #
|
|
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/
|
|
94
|
-
`category` · `defaultState: "
|
|
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:
|
|
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`, ❌
|
|
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
|
|
141
|
-
picker is owned by the
|
|
142
|
-
"arrives". A card's Edit stages its data into the
|
|
143
|
-
- **
|
|
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
|
|
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
|
-
|
|
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>/` = `
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
`
|
|
136
|
-
2. **Rename the
|
|
137
|
-
becomes `lyceum-chat-layout` in both places.
|
|
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
|
|
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.**
|
|
147
|
-
A copied
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
|
79
|
-
WHOLE vocabulary: everything
|
|
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
|
|
82
|
-
make it public; if 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
|
|
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);
|
|
100
|
-
- **Arrival: nobody sends a state.** The host
|
|
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
|
|
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
|
-
`
|
|
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
|
|
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
|
|
143
|
-
Durable slices also opt out of `cancelBelow` retraction when a higher
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/local/{sample-template/chat → sample-component/text-chat}/components/assistant-turn.yaml
RENAMED
|
File without changes
|
|
File without changes
|
/package/local/{sample-template/chat → sample-component/text-chat}/components/thinking-dots.yaml
RENAMED
|
File without changes
|
/package/local/{sample-template/chat → sample-component/text-chat}/components/user-turn.yaml
RENAMED
|
File without changes
|