@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/bin/studio.mjs
CHANGED
|
@@ -141,7 +141,7 @@ async function scaffoldIfEmpty() {
|
|
|
141
141
|
// identical (local/scaffold.mjs). rx/<name>/ stays nested: every subfolder of rx/ IS a
|
|
142
142
|
// project (base collect.ts listProjects) and its folder name is the `org` that
|
|
143
143
|
// publishes, so a flat rx/ could hold exactly one project and would read `components`
|
|
144
|
-
// and `
|
|
144
|
+
// and `apps` as orgs.
|
|
145
145
|
createFirstProject(home, name);
|
|
146
146
|
console.log(`\n Created design/${name}/, prompts/ and nodes/ in ${cwd}\n`);
|
|
147
147
|
}
|
package/local/catalog.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { existsSync } from "fs";
|
|
|
18
18
|
type Loaders = {
|
|
19
19
|
listDefinitions: (kind: string, org?: string) => unknown[];
|
|
20
20
|
listStates: (kind: string, name: string) => unknown[];
|
|
21
|
-
|
|
21
|
+
listAppsForWorkbench: (org?: string) => unknown[];
|
|
22
22
|
loadDefinition: (ref: string, kind: string) => unknown;
|
|
23
23
|
listOrgs: () => string[];
|
|
24
24
|
inputPropKeys: (props: unknown) => string[];
|
|
@@ -98,7 +98,7 @@ export async function loadCatalog(
|
|
|
98
98
|
return {
|
|
99
99
|
listDefinitions: defs.listDefinitions as Loaders["listDefinitions"],
|
|
100
100
|
listStates: defs.listStates as Loaders["listStates"],
|
|
101
|
-
|
|
101
|
+
listAppsForWorkbench: defs.listAppsForWorkbench as Loaders["listAppsForWorkbench"],
|
|
102
102
|
loadDefinition: defs.loadDefinition as Loaders["loadDefinition"],
|
|
103
103
|
listOrgs: defs.listOrgs as Loaders["listOrgs"],
|
|
104
104
|
inputPropKeys: inputs.inputPropKeys as Loaders["inputPropKeys"],
|
package/local/mcp.ts
CHANGED
|
@@ -155,8 +155,9 @@ export function buildTools(deps: McpDeps, appRoot: string): Record<string, Tool>
|
|
|
155
155
|
run: async ({ uri }) => {
|
|
156
156
|
needsBinding();
|
|
157
157
|
const c = await deps.catalog();
|
|
158
|
-
|
|
159
|
-
const
|
|
158
|
+
// `.../definition` is a suffix on the app's URI, not part of its ref.
|
|
159
|
+
const ref = String(uri).replace(/^unoverse:\/\/(components|atoms|apps)\//, "").replace(/\/definition$/, "");
|
|
160
|
+
const kind = /\/apps\//.test(String(uri)) ? "template" : /\/atoms\//.test(String(uri)) ? "atom" : "component";
|
|
160
161
|
return { definition: c.loadDefinition(ref, kind), files: deps.sourceFiles(String(uri)) };
|
|
161
162
|
},
|
|
162
163
|
},
|
package/local/plugin.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Routes mirror the server's shapes exactly, because the screens parse those shapes:
|
|
10
10
|
* GET /dev/orgs GET /dev/themes?org= GET /dev/components?org=
|
|
11
|
-
* GET /dev/
|
|
11
|
+
* GET /dev/apps?org= GET /dev/atoms
|
|
12
12
|
* GET /local/theme?ref= GET /local/definition?uri=
|
|
13
13
|
* GET /skills /skills/:name GET /prompt-blocks
|
|
14
14
|
* GET|POST /registry/state (no database locally, see below)
|
|
@@ -29,10 +29,10 @@ import { atomNames, vocabulary } from "./authoring.js";
|
|
|
29
29
|
import { buildTools, handleMcp, mintPairingCode } from "./mcp.js";
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* A definition URI's home on disk: a component's folder, or
|
|
32
|
+
* A definition URI's home on disk: a component's folder, or an app's.
|
|
33
33
|
*
|
|
34
34
|
* Resolution is the loaders', not this file's: `componentFolder` already knows a
|
|
35
|
-
* component lives in either tier and that names are unique across them.
|
|
35
|
+
* component lives in either tier and that names are unique across them. Apps are
|
|
36
36
|
* manifest-only under their own project, which `projectDir` locates.
|
|
37
37
|
*/
|
|
38
38
|
function definitionHome(
|
|
@@ -40,13 +40,15 @@ function definitionHome(
|
|
|
40
40
|
c: { componentFolder: (ref: string) => string | null; projectDir: (n: string) => string; listOrgs: () => string[] },
|
|
41
41
|
projectRoot: string,
|
|
42
42
|
): string | null {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// An app definition is addressed as `unoverse://apps/<org>/<name>/definition`, so the
|
|
44
|
+
// suffix has to come off before the rest is read as `<org>/<name>`.
|
|
45
|
+
const ref = uri.replace(/^unoverse:\/\/(components|atoms|apps)\//, "").replace(/\/definition$/, "");
|
|
46
|
+
if (/\/apps\//.test(uri)) {
|
|
45
47
|
const i = ref.indexOf("/");
|
|
46
48
|
const [org, name] = i === -1 ? [null, ref] : [ref.slice(0, i), ref.slice(i + 1)];
|
|
47
49
|
const orgs = org ? [org] : c.listOrgs();
|
|
48
50
|
for (const o of orgs) {
|
|
49
|
-
const dir = resolve(c.projectDir(o), "
|
|
51
|
+
const dir = resolve(c.projectDir(o), "apps", name.toLowerCase());
|
|
50
52
|
if (existsSync(dir)) return dir;
|
|
51
53
|
}
|
|
52
54
|
return null;
|
|
@@ -346,8 +348,8 @@ export function localStudioHost(): Plugin {
|
|
|
346
348
|
}),
|
|
347
349
|
});
|
|
348
350
|
|
|
349
|
-
case path === "/dev/
|
|
350
|
-
return json(res, {
|
|
351
|
+
case path === "/dev/apps":
|
|
352
|
+
return json(res, { apps: c.listAppsForWorkbench(org) });
|
|
351
353
|
|
|
352
354
|
case path === "/dev/atoms":
|
|
353
355
|
return json(res, { atoms: c.listDefinitions("atom") });
|
|
@@ -360,14 +362,14 @@ export function localStudioHost(): Plugin {
|
|
|
360
362
|
case path === "/local/definition": {
|
|
361
363
|
const uri = url.searchParams.get("uri") ?? "";
|
|
362
364
|
// Apps are addressed both ways (unoverse://apps/<org>/<id> and
|
|
363
|
-
// unoverse://
|
|
364
|
-
const kind = /\/
|
|
365
|
+
// unoverse://apps/<org>/<id>/definition); both load the app kind.
|
|
366
|
+
const kind = /\/apps\//.test(uri) ? "template" : /\/atoms\//.test(uri) ? "atom" : "component";
|
|
365
367
|
// A trailing `@form` is a request for a VARIANT of the definition, not part of
|
|
366
368
|
// its name. This door did not know that convention and looked up the whole
|
|
367
369
|
// string, so every component read as `<name>@refs` answered "definition not
|
|
368
370
|
// found". Strip any variant: this route serves the one form it has, which is
|
|
369
371
|
// also the only form the server serves.
|
|
370
|
-
const ref = uri.replace(/^unoverse:\/\/(components|atoms|apps
|
|
372
|
+
const ref = uri.replace(/^unoverse:\/\/(components|atoms|apps)\//, "").replace(/\/definition$/, "").replace(/@[a-z]+$/, "");
|
|
371
373
|
const d = c.loadDefinition(ref, kind);
|
|
372
374
|
return d ? json(res, d) : json(res, { error: `definition not found: ${uri}` }, 404);
|
|
373
375
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# THE CHAT REGION — one shared component (text-chat) owns the whole chat CX: the
|
|
2
|
+
# welcome hero on an empty chat, the live timeline once messages exist, the suggestion
|
|
3
|
+
# chips and the composer. The switching rides the projected conversation facts inside
|
|
4
|
+
# the component; nothing is wired here. Shared by core and core-focus.
|
|
5
|
+
#
|
|
6
|
+
# A BARE ref resolves your own project's component: a ref inside a project's tree
|
|
7
|
+
# matches that project first. Qualify it (`<project>/text-chat`) only if you ship two
|
|
8
|
+
# projects that both carry the name.
|
|
9
|
+
type: Ref
|
|
10
|
+
ref: text-chat
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# THE CHAT COLUMN WITH A FOCUSED COMPONENT OVER IT — core, plus the focus surface
|
|
2
|
+
# laid over it. A core VARIANT, not a copy: chat-region is the same shared file
|
|
3
|
+
# core.yaml includes; the only difference is the overlay slot.
|
|
4
|
+
type: Box
|
|
5
|
+
appWidth: chat
|
|
6
|
+
style:
|
|
7
|
+
direction: column
|
|
8
|
+
minHeight: "0"
|
|
9
|
+
children:
|
|
10
|
+
- type: Box
|
|
11
|
+
style:
|
|
12
|
+
direction: column
|
|
13
|
+
flex: 1
|
|
14
|
+
minHeight: "0"
|
|
15
|
+
position: relative
|
|
16
|
+
children:
|
|
17
|
+
- $include: components/chat-region
|
|
18
|
+
- $include: components/focus-overlay
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# A GENERIC CHAT — a worked example
|
|
1
|
+
# A GENERIC CHAT — a worked example app, shipped with Studio so apps/ is
|
|
2
2
|
# never an empty folder. It is yours: rename it, restyle it (it uses only foundation
|
|
3
3
|
# tokens, so your project's theme restyles it whole), or delete it.
|
|
4
|
-
# Guide: https://docs.unoverse.ai/design/
|
|
4
|
+
# Guide: https://docs.unoverse.ai/design/apps.md
|
|
5
5
|
name: chat
|
|
6
6
|
description: A conversational assistant surface with a welcome hero, suggestions, and a focus overlay.
|
|
7
7
|
whenToUse: Talk to the assistant — ask a question, get help, or start any task in conversation. The conversational home and fallback surface.
|
|
8
8
|
category: Assistant
|
|
9
9
|
version: 1.0.0
|
|
10
|
-
defaultState:
|
|
10
|
+
defaultState: app
|
|
11
11
|
inputSchema:
|
|
12
12
|
type: object
|
|
13
13
|
properties:
|
|
@@ -16,22 +16,17 @@ inputSchema:
|
|
|
16
16
|
description: The user's request
|
|
17
17
|
|
|
18
18
|
# THE TEMPLATE TREE (State Model v2): one declaration, everything follows.
|
|
19
|
-
# Top-level order IS the priority ladder (base first, then reaction states).
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# and legitimately drawn under the focus overlay too. Declaring it would contain it to
|
|
26
|
-
# standard and empty the chat column in every reaction state.
|
|
19
|
+
# Top-level order IS the priority ladder (base first, then reaction states). No
|
|
20
|
+
# stateOrder, no hand-written guards.
|
|
21
|
+
# NOTE: the chat itself is not a state here. Its two moods (welcome hero / live
|
|
22
|
+
# timeline) live inside the text-chat component, which both cores draw via
|
|
23
|
+
# components/chat-region. The component defaults to welcome on an empty conversation
|
|
24
|
+
# and hands over to the timeline the moment messages exist.
|
|
27
25
|
states:
|
|
28
|
-
standard:
|
|
29
|
-
states:
|
|
30
|
-
welcome: {}
|
|
26
|
+
standard: {}
|
|
31
27
|
focused: {}
|
|
32
28
|
|
|
33
29
|
preview:
|
|
34
|
-
conversation: [ markdown-renderer ]
|
|
35
30
|
focused: [ card ]
|
|
36
31
|
|
|
37
32
|
autoTrigger: false
|
package/local/{sample-template/chat → sample-component/text-chat}/components/suggestions.yaml
RENAMED
|
@@ -32,7 +32,7 @@ children:
|
|
|
32
32
|
children:
|
|
33
33
|
- type: Button
|
|
34
34
|
action:
|
|
35
|
-
type:
|
|
35
|
+
type: setAppValue
|
|
36
36
|
values:
|
|
37
37
|
- key: openPanel
|
|
38
38
|
value: null
|
|
@@ -70,7 +70,7 @@ children:
|
|
|
70
70
|
action:
|
|
71
71
|
type: sendMessage
|
|
72
72
|
then:
|
|
73
|
-
type:
|
|
73
|
+
type: setAppValue
|
|
74
74
|
values:
|
|
75
75
|
- key: openPanel
|
|
76
76
|
value: null
|
|
@@ -110,7 +110,7 @@ children:
|
|
|
110
110
|
children:
|
|
111
111
|
- type: Button
|
|
112
112
|
action:
|
|
113
|
-
type:
|
|
113
|
+
type: setAppValue
|
|
114
114
|
values:
|
|
115
115
|
- key: openPanel
|
|
116
116
|
value: faq
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# THE SHELL of the one public state: the FULL chat CX — the header, the mood region
|
|
2
|
+
# (welcome hero / live timeline), the suggestion chips over its foot, and the composer.
|
|
3
|
+
# The steps switch on chatState (the declared axis): Studio writes it when you step
|
|
4
|
+
# Welcome/Active. Live, nothing writes it, so the Switch falls to its default case,
|
|
5
|
+
# where the projected conversation facts pick the mood.
|
|
6
|
+
type: Box
|
|
7
|
+
style:
|
|
8
|
+
direction: column
|
|
9
|
+
# `flex: 1` fills a flex column (the template's core); `height: full` fills a block
|
|
10
|
+
# parent with a real height (the standalone preview).
|
|
11
|
+
flex: 1
|
|
12
|
+
height: full
|
|
13
|
+
minHeight: "0"
|
|
14
|
+
children:
|
|
15
|
+
- $include: components/header
|
|
16
|
+
- type: Box
|
|
17
|
+
style:
|
|
18
|
+
direction: column
|
|
19
|
+
flex: 1
|
|
20
|
+
minHeight: "0"
|
|
21
|
+
position: relative
|
|
22
|
+
when:
|
|
23
|
+
- field: hasMessages
|
|
24
|
+
eq: true
|
|
25
|
+
apply:
|
|
26
|
+
background: surface.base
|
|
27
|
+
radiusTopLeft: 2xl
|
|
28
|
+
overflow: hidden
|
|
29
|
+
- field: chatState
|
|
30
|
+
eq: active
|
|
31
|
+
apply:
|
|
32
|
+
background: surface.base
|
|
33
|
+
radiusTopLeft: 2xl
|
|
34
|
+
overflow: hidden
|
|
35
|
+
# chatState is only ever set by Studio's stepper, so this height is
|
|
36
|
+
# preview-only: live, the template's flex sizes the region.
|
|
37
|
+
minHeight: "120"
|
|
38
|
+
children:
|
|
39
|
+
- type: Switch
|
|
40
|
+
on: chatState
|
|
41
|
+
cases:
|
|
42
|
+
welcome: { $include: layouts/chat-welcome }
|
|
43
|
+
active: { $include: layouts/chat-active }
|
|
44
|
+
default:
|
|
45
|
+
type: Box
|
|
46
|
+
style:
|
|
47
|
+
direction: column
|
|
48
|
+
flex: 1
|
|
49
|
+
minHeight: "0"
|
|
50
|
+
children:
|
|
51
|
+
- type: Box
|
|
52
|
+
visibleWhen: isEmpty
|
|
53
|
+
style:
|
|
54
|
+
direction: column
|
|
55
|
+
flex: 1
|
|
56
|
+
minHeight: "0"
|
|
57
|
+
children:
|
|
58
|
+
- $include: layouts/chat-welcome
|
|
59
|
+
- type: Box
|
|
60
|
+
visibleWhen: hasMessages
|
|
61
|
+
style:
|
|
62
|
+
direction: column
|
|
63
|
+
flex: 1
|
|
64
|
+
minHeight: "0"
|
|
65
|
+
children:
|
|
66
|
+
- $include: layouts/chat-active
|
|
67
|
+
- type: Box
|
|
68
|
+
style:
|
|
69
|
+
position: absolute
|
|
70
|
+
bottom: "0"
|
|
71
|
+
left: "0"
|
|
72
|
+
right: "0"
|
|
73
|
+
direction: column
|
|
74
|
+
children:
|
|
75
|
+
- $include: components/suggestions
|
|
76
|
+
- type: Ref
|
|
77
|
+
ref: composer-bar
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
title: Text Chat
|
|
2
|
+
description: The conversation surface, a welcome hero on an empty chat and the live message timeline once it begins.
|
|
3
|
+
whenToUse: Start talking to the assistant and see the conversation itself, the welcome screen to begin from, then the running exchange of messages once the chat is underway. Not for typing a message and not a page or card of its own, it is the chat surface inside your own apps.
|
|
4
|
+
category: Communication
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
|
|
7
|
+
# The app-level LOAD MODE: absent, a component app would load in `focus`. Arrival
|
|
8
|
+
# inside a template is the tree's first declared state (chat); this key keeps the app
|
|
9
|
+
# load honest to it.
|
|
10
|
+
defaultState: chat
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# THE CHAT SURFACE — a worked example component, shipped with Studio so the chat CX
|
|
2
|
+
# lives in ONE place your apps can share. It is yours: rename it, restyle it (it uses
|
|
3
|
+
# only foundation tokens, so your project's theme restyles it whole), or delete it.
|
|
4
|
+
# Guide: https://docs.unoverse.ai/design/components.md
|
|
5
|
+
unoverse: "1.0"
|
|
6
|
+
kind: component
|
|
7
|
+
name: text-chat
|
|
8
|
+
category: Communication
|
|
9
|
+
nodeSize:
|
|
10
|
+
width: 480
|
|
11
|
+
height: 400
|
|
12
|
+
props:
|
|
13
|
+
# `input: false` on all three — no workflow fills them, so none of them join the
|
|
14
|
+
# node's config contract. They exist so the preview has something to draw.
|
|
15
|
+
#
|
|
16
|
+
# chatState is THE PREVIEW AXIS: Studio writes it when you step Welcome/Active. The
|
|
17
|
+
# default is EMPTY on purpose — live, nothing writes it, the Switch falls to its
|
|
18
|
+
# default case and the conversation facts pick the mood. A non-empty default would
|
|
19
|
+
# pin the mood forever.
|
|
20
|
+
chatState:
|
|
21
|
+
type: string
|
|
22
|
+
input: false
|
|
23
|
+
default: ""
|
|
24
|
+
enum: ["", welcome, active]
|
|
25
|
+
# Preview defaults for the live fallback's guards (the conversation facts the
|
|
26
|
+
# platform projects for real).
|
|
27
|
+
isEmpty:
|
|
28
|
+
type: boolean
|
|
29
|
+
input: false
|
|
30
|
+
default: true
|
|
31
|
+
hasMessages:
|
|
32
|
+
type: boolean
|
|
33
|
+
input: false
|
|
34
|
+
default: false
|
|
35
|
+
# THE STATE TREE (State Model v2): ONE public state — the single-state permanent
|
|
36
|
+
# component: it lives in a template's core and templates never rearrange around it.
|
|
37
|
+
# The chat's two moods NEST beneath it as PRIVATE substates (the rearrange rule: the
|
|
38
|
+
# template wouldn't move, so they nest), which is also what makes them steppable in
|
|
39
|
+
# Studio. The mood is DERIVED from the conversation, so the shell's default case keeps
|
|
40
|
+
# its own conditions. The template has the state; this component just reacts.
|
|
41
|
+
state:
|
|
42
|
+
view:
|
|
43
|
+
states:
|
|
44
|
+
chat:
|
|
45
|
+
on: chatState
|
|
46
|
+
states:
|
|
47
|
+
welcome:
|
|
48
|
+
layout: chat-welcome
|
|
49
|
+
active:
|
|
50
|
+
layout: chat-active
|
|
51
|
+
root:
|
|
52
|
+
type: Switch
|
|
53
|
+
on: view
|
|
54
|
+
cases:
|
|
55
|
+
chat: { $include: layouts/chat }
|
|
56
|
+
# Ref placement: embedded in a template's core the component has no spawned
|
|
57
|
+
# instance slice, so `view` is absent from scope — the chat is still the one
|
|
58
|
+
# thing to draw.
|
|
59
|
+
default: { $include: layouts/chat }
|
package/local/scaffold.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creating a project, from either end.
|
|
3
3
|
*
|
|
4
|
-
* A project (an "org") is `design/<name>/` holding components, styles and
|
|
5
|
-
*
|
|
4
|
+
* A project (an "org") is `design/<name>/` holding components, styles and apps.
|
|
5
|
+
* It is the unit a developer authors in and the unit that publishes: every row
|
|
6
6
|
* that lands in a universe carries its name in the `org` column.
|
|
7
7
|
*
|
|
8
8
|
* ONE IMPLEMENTATION, TWO CALLERS. `bin/studio.mjs` creates the FIRST project at launch,
|
|
@@ -73,7 +73,7 @@ export function createProject(home, name) {
|
|
|
73
73
|
const root = join(designHome(home), name);
|
|
74
74
|
if (existsSync(root)) throw new Error(`${name} already exists in this workspace`);
|
|
75
75
|
|
|
76
|
-
for (const d of ["components/welcome", "
|
|
76
|
+
for (const d of ["components/welcome", "apps", "styles"]) mkdirSync(join(root, d), { recursive: true });
|
|
77
77
|
writeFileSync(join(root, "components/welcome/welcome.yaml"), WELCOME);
|
|
78
78
|
|
|
79
79
|
// Worked example styles (sample-styles: brand ingredients + the override rules), so
|
|
@@ -86,13 +86,18 @@ export function createProject(home, name) {
|
|
|
86
86
|
cpSync(sampleStyles, join(root, "styles"), { recursive: true });
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// A worked example
|
|
90
|
-
//
|
|
91
|
-
// templates/ is never an empty folder and the first app is a rename away. Same
|
|
89
|
+
// A worked example APP (a generic chat: focus overlay + the chat region),
|
|
90
|
+
// so apps/ is never an empty folder and the first app is a rename away. Same
|
|
92
91
|
// rule as the styles: copied only into an empty folder, then the developer's.
|
|
93
|
-
const
|
|
94
|
-
if (existsSync(
|
|
95
|
-
cpSync(
|
|
92
|
+
const sampleApp = join(dirname(fileURLToPath(import.meta.url)), "sample-app/chat");
|
|
93
|
+
if (existsSync(sampleApp) && readdirSync(join(root, "apps")).length === 0) {
|
|
94
|
+
cpSync(sampleApp, join(root, "apps/chat"), { recursive: true });
|
|
95
|
+
// Its other half: the chat CX itself (welcome hero, timeline, suggestions,
|
|
96
|
+
// composer) is a COMPONENT the app draws via `Ref`, so the surface lives once
|
|
97
|
+
// and every app of this project shares it. Copied with the app, never without
|
|
98
|
+
// it: an app whose chat-region resolves nothing renders an empty column.
|
|
99
|
+
const sampleComponent = join(dirname(fileURLToPath(import.meta.url)), "sample-component/text-chat");
|
|
100
|
+
if (existsSync(sampleComponent)) cpSync(sampleComponent, join(root, "components/text-chat"), { recursive: true });
|
|
96
101
|
}
|
|
97
102
|
return root;
|
|
98
103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unoverse-platform/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
|
|
6
6
|
"scripts": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"@unoverse-platform/base": "^0.3.37",
|
|
30
30
|
"@unoverse-platform/marketplace": "*",
|
|
31
31
|
"@vitejs/plugin-react": "^4.3.4",
|
|
32
|
+
"lucide-react": "^0.546.0",
|
|
32
33
|
"markdown-to-jsx": "^7.7.17",
|
|
33
34
|
"oidc-client-ts": "^3.4.1",
|
|
34
35
|
"react": "^18.2.0",
|
|
@@ -715,9 +715,16 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
715
715
|
</>
|
|
716
716
|
)}
|
|
717
717
|
<div className={isFocused ? "min-h-0 flex-1 overflow-auto" : "min-w-0 flex-1 overflow-auto p-10"}>
|
|
718
|
+
{/* THE STAGE OFFERS ITS HEIGHT; the component decides whether to take it.
|
|
719
|
+
A single-row GRID, not a flex column: the SDK's shadow host is
|
|
720
|
+
`display: contents`, so `.uno-isolate-root` is the item here, and a grid
|
|
721
|
+
item stretches on BOTH axes by default — a flex item only stretches
|
|
722
|
+
across. That gives the isolate root a real height to resolve against,
|
|
723
|
+
so a full surface (chat, voice) fills and a card still sizes to its
|
|
724
|
+
content. */}
|
|
718
725
|
<div
|
|
719
726
|
key={`${masterState}:${previewNonce}`}
|
|
720
|
-
className={`mx-auto max-w-full ${isFocused ? "h-full" : "transition-[width] duration-200"}`}
|
|
727
|
+
className={`mx-auto max-w-full ${isFocused ? "h-full" : "grid h-full grid-rows-1 transition-[width] duration-200"}`}
|
|
721
728
|
style={{ width: sourceOpen ? "100%" : (stageWidth ?? "100%"), ...(isFocused ? { animation: "wb-focusFadeIn 0.2s ease-out" } : {}) }}
|
|
722
729
|
>
|
|
723
730
|
{selected && <UnoverseComponent client={client} store={store} chatId={CHAT_ID} nodeId={selected} theme={theme} onAction={(a) => setLastAction(typeof a === "string" ? a : a?.type ?? null)} />}
|
|
@@ -18,7 +18,7 @@ const APP_UI_URI = "ui://unoverse/app.html";
|
|
|
18
18
|
export function NativeAppHost({
|
|
19
19
|
server,
|
|
20
20
|
token,
|
|
21
|
-
|
|
21
|
+
appId,
|
|
22
22
|
userId,
|
|
23
23
|
conversationId,
|
|
24
24
|
onStatus,
|
|
@@ -26,7 +26,7 @@ export function NativeAppHost({
|
|
|
26
26
|
/** Universe origin, e.g. http://localhost:4105 — the /mcp and /stream host. */
|
|
27
27
|
server: string;
|
|
28
28
|
token?: string;
|
|
29
|
-
|
|
29
|
+
appId?: string;
|
|
30
30
|
userId: string;
|
|
31
31
|
conversationId: string;
|
|
32
32
|
/** The iframe app owns the ONLY live connection — it posts `unoverse:status`
|
|
@@ -53,7 +53,7 @@ export function NativeAppHost({
|
|
|
53
53
|
return () => window.removeEventListener("message", onSize);
|
|
54
54
|
}, [onStatus]);
|
|
55
55
|
// A different app starts back at the host default until it reports.
|
|
56
|
-
useEffect(() => setAppWidth(null), [
|
|
56
|
+
useEffect(() => setAppWidth(null), [appId]);
|
|
57
57
|
|
|
58
58
|
// Pull the app shell from the universe — one resources/read, then the client closes.
|
|
59
59
|
useEffect(() => {
|
|
@@ -110,13 +110,13 @@ then come back — Studio picks the session up by itself.`
|
|
|
110
110
|
frame.current?.contentWindow?.postMessage(
|
|
111
111
|
{
|
|
112
112
|
type: "unoverse:config",
|
|
113
|
-
config: { serverUrl: server, apiUrl: server,
|
|
113
|
+
config: { serverUrl: server, apiUrl: server, appId, userId, conversationId, token },
|
|
114
114
|
},
|
|
115
115
|
"*",
|
|
116
116
|
);
|
|
117
117
|
};
|
|
118
118
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
-
useEffect(postConfig, [token,
|
|
119
|
+
useEffect(postConfig, [token, appId, userId, conversationId, server, html]);
|
|
120
120
|
|
|
121
121
|
if (error)
|
|
122
122
|
return (
|