@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
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 `templates` as orgs.
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
- listTemplatesForWorkbench: (org?: string) => unknown[];
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
- listTemplatesForWorkbench: defs.listTemplatesForWorkbench as Loaders["listTemplatesForWorkbench"],
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
- const ref = String(uri).replace(/^unoverse:\/\/(components|atoms|apps|templates)\//, "");
159
- const kind = /\/(apps|templates)\//.test(String(uri)) ? "template" : /\/atoms\//.test(String(uri)) ? "atom" : "component";
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/templates?org= GET /dev/atoms
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 a template's.
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. Templates are
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
- const ref = uri.replace(/^unoverse:\/\/(components|atoms|apps|templates)\//, "");
44
- if (/\/(apps|templates)\//.test(uri)) {
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), "templates", name.toLowerCase());
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/templates":
350
- return json(res, { templates: c.listTemplatesForWorkbench(org) });
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://templates/<org>/<id>); both load the template kind.
364
- const kind = /\/(apps|templates)\//.test(uri) ? "template" : /\/atoms\//.test(uri) ? "atom" : "component";
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|templates)\//, "").replace(/@[a-z]+$/, "");
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
@@ -0,0 +1,14 @@
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/chat-region
@@ -1,13 +1,13 @@
1
- # A GENERIC CHAT — a worked example template, shipped with Studio so templates/ is
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/templates.md
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: template
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). NESTING
20
- # is containment: welcome exists only inside standard — the compiler strips it from
21
- # every other arrangement — and it is the landing default. No stateOrder, no
22
- # hand-written guards.
23
- # NOTE: the conversation timeline is NOT declared as a substate — it is standard's own
24
- # shell content (states/conversation), included by the shared chrome (components/core)
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
@@ -32,7 +32,7 @@ children:
32
32
  children:
33
33
  - type: Button
34
34
  action:
35
- type: setTemplateValue
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: setTemplateValue
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: setTemplateValue
113
+ type: setAppValue
114
114
  values:
115
115
  - key: openPanel
116
116
  value: faq
@@ -1,5 +1,4 @@
1
1
  type: Box
2
- visibleWhen: hasMessages
3
2
  autoScroll: true
4
3
  style:
5
4
  flex: 1
@@ -1,5 +1,4 @@
1
1
  type: Box
2
- visibleWhen: isEmpty
3
2
  style:
4
3
  direction: column
5
4
  flex: 1
@@ -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 }
@@ -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
- * templates. It is the unit a developer authors in and the unit that publishes: every row
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", "templates", "styles"]) mkdirSync(join(root, d), { recursive: true });
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 TEMPLATE (a generic chat: welcome hero, suggestions, focus
90
- // overlay — the bpp-chat-layout shape, neutralized to foundation tokens), so
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 sampleTemplate = join(dirname(fileURLToPath(import.meta.url)), "sample-template/chat");
94
- if (existsSync(sampleTemplate) && readdirSync(join(root, "templates")).length === 0) {
95
- cpSync(sampleTemplate, join(root, "templates/chat"), { recursive: true });
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.68",
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
- templateId,
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
- templateId?: string;
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), [templateId]);
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, templateId, userId, conversationId, token },
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, templateId, userId, conversationId, server, html]);
119
+ useEffect(postConfig, [token, appId, userId, conversationId, server, html]);
120
120
 
121
121
  if (error)
122
122
  return (