@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
@@ -3,15 +3,15 @@
3
3
  * ⛔ THIS FILE IS NOT TO BE EDITED. ⛔
4
4
  *
5
5
  * Every single change in this file — and any change made on behalf of a
6
- * component or template that seems to need one here — MUST be confirmed by
6
+ * component or app that seems to need one here — MUST be confirmed by
7
7
  * Gavin first. No exceptions: not bug fixes, not one-liners, not reverts.
8
8
  * State the intended diff and wait for an explicit yes.
9
9
  *
10
- * Components and templates are DATA (design/). If a template seems to need code
10
+ * Components and apps are DATA (design/). If an app seems to need code
11
11
  * here, the answer is almost always to express it as data — or to ask.
12
12
  * ============================================================================
13
13
  *
14
- * Templates view — renders a TEMPLATE definition (layout) and fills its
14
+ * Apps view — renders an APP definition (layout) and fills its
15
15
  * ComponentSlots from a mock conversation seeded into the store.
16
16
  *
17
17
  * This is the visual proof of the slot system: select KeyService → its
@@ -23,7 +23,7 @@ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from
23
23
  import { useAuth } from "react-oidc-context";
24
24
  import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
25
25
  import { ComponentStore } from "@unoverse/sdk";
26
- import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resolveActiveLayout, visible, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
26
+ import { StreamedUnoverseApp, useUnoverseConnection, computeAppWidth, resolveActiveLayout, visible, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
27
27
  import { NativeAppHost } from "./NativeAppHost";
28
28
  import { inArrangement, viewOfSlice } from "./states";
29
29
  import { client, authedFetch, hasAuth, hasSourceAccess, UNOVERSE_BASE, liveUniverseUrl } from "studio-host";
@@ -80,36 +80,36 @@ interface Def {
80
80
  // The client org this app belongs to (server-injected) — names its theme (`<org>/light`).
81
81
  org?: string;
82
82
  description?: string;
83
- // A native service the template binds (UNOVERSE_SPEC §2e-1) — e.g. "voice". The channel
84
- // instantiates the matching service and feeds its neutral state + actions to the template.
83
+ // A native service the app binds (UNOVERSE_SPEC §2e-1) — e.g. "voice". The channel
84
+ // instantiates the matching service and feeds its neutral state + actions to the app.
85
85
  service?: string;
86
86
  props?: Record<string, PropDef>;
87
- // Present when this template is an MCP app: the workflow it calls (from its manifest).
87
+ // Present when this app is an MCP app: the workflow it calls (from its manifest).
88
88
  binding?: { workflow?: string; trigger?: string };
89
89
  // true = on load, also fire the workflow (Mode B); false/absent = just load (Mode A).
90
90
  autoTrigger?: boolean;
91
91
  // MOCK-preview hint: the component(s) this app's workflow streams. When present, the
92
- // mock seeds ONLY these (so a template previews the real app). Absent → seed NOTHING:
93
- // the template renders its own default/welcome state (the default view), not a dump of
92
+ // mock seeds ONLY these (so the preview matches the real app). Absent → seed NOTHING:
93
+ // the app renders its own default/welcome state (the default view), not a dump of
94
94
  // the whole component catalog.
95
95
  previewComponents?: string[];
96
- // Layout height mode. Absent/true = FLUID (the template fills the stage height — chat /
96
+ // Layout height mode. Absent/true = FLUID (the app fills the stage height — chat /
97
97
  // assistant / voice). false = size to CONTENT (the frame shrinks to the rendered
98
98
  // component — focused widget apps like Bank Transfer).
99
99
  fluidHeight?: boolean;
100
- // The template's layer views, enumerated from its `states/` folder (UNOVERSE_LAYERS.md §7).
100
+ // The app's layer views, enumerated from its `states/` folder (UNOVERSE_LAYERS.md §7).
101
101
  // `when` = the state's root visibleWhen selector; `where` = a reaction-contract surface's
102
102
  // component-state selector (STATE_MODEL §5b) — the viewer lists AND activates by them.
103
103
  states?: { name: string; when?: unknown; where?: unknown }[];
104
- // TEMPLATE LAYOUTS (name-sync, docs/design/05): the composed arrangements of a
105
- // multi-layout template + its default. The viewer shows these as a pill row (the
106
- // template-tier twin of a component's faces); activation = the same selector a
104
+ // APP LAYOUTS (name-sync, docs/design/05): the composed arrangements of a
105
+ // multi-layout app + its default. The viewer shows these as a pill row (the
106
+ // app-tier twin of a component's faces); activation = the same selector a
107
107
  // reaction surface uses (put a component into the view of the layout's name).
108
108
  layouts?: Record<string, unknown>;
109
109
  defaultLayout?: string;
110
110
  // The definition tree (components: used to resolve a state's Switch discriminant).
111
111
  root?: unknown;
112
- // The full MCP-app manifest (present when the template folder carries manifest.json).
112
+ // The full MCP-app manifest (present when the app folder carries manifest.json).
113
113
  app?: {
114
114
  name: string;
115
115
  description?: string;
@@ -124,13 +124,13 @@ interface Def {
124
124
  // Per-STATE mock seeding: `{ "<state>": ["course-card", "course-card"] }` — the
125
125
  // author decides what each state's preview shows (a repeated name = several
126
126
  // instances). Supersedes the legacy flat `previewComponents`.
127
- // Per-state mock: an ARRAY seeds components; an OBJECT is authored template-state
127
+ // Per-state mock: an ARRAY seeds components; an OBJECT is authored app-state
128
128
  // data (what the workflow would have echoed) merged verbatim on pick.
129
129
  preview?: Record<string, string[] | Record<string, unknown>>;
130
130
  previewComponents?: string[];
131
131
  fluidHeight?: boolean;
132
132
  // The app's named default state on load (the single authored field, OPEN name):
133
- // "template" = fluid · "focus" = fit content + opens in focus on load · "component" =
133
+ // "app" = fluid · "focus" = fit content + opens in focus on load · "component" =
134
134
  // inline card · anything future. `fluidHeight` above is derived from it.
135
135
  // `mode` = pre-rename alias, read as fallback.
136
136
  defaultState?: string;
@@ -149,7 +149,7 @@ export function TemplatesView({
149
149
  theme: defaultTheme,
150
150
  themeName,
151
151
  org = "all",
152
- // Templates open LIVE by default (Gavin, 2026-08-08): an app IS its live binding —
152
+ // Apps open LIVE by default (Gavin, 2026-08-08): an app IS its live binding —
153
153
  // Mock is the authoring affordance you step INTO (states are mock-only), not the
154
154
  // resting mode.
155
155
  defaultLive = true,
@@ -167,13 +167,13 @@ export function TemplatesView({
167
167
  const available = useRegistryToggles("template");
168
168
  const [templates, setTemplates] = useState<Def[]>([]);
169
169
  const [components, setComponents] = useState<Def[]>([]);
170
- const [selected, setSelected] = usePersistentState<string | null>("wb:templates:selected", null);
171
- // A fresh store per mode AND per app selection: a catch-all template accumulates
170
+ const [selected, setSelected] = usePersistentState<string | null>("wb:apps:selected", null);
171
+ // A fresh store per mode AND per app selection: a catch-all app accumulates
172
172
  // whatever's seeded, so switching apps must start clean (no stale components linger).
173
173
  // The active layer view (Storybook-style state picker). Recreating the store on change
174
174
  // starts it clean so the seed effect re-derives the state (UNOVERSE_LAYERS.md §7).
175
175
  const [activeState, setActiveState] = useState<{ name: string; when?: unknown; where?: unknown } | null>(null);
176
- // Selecting a template lands on its FIRST state — the def's natural default (a voice
176
+ // Selecting an app lands on its FIRST state — the def's natural default (a voice
177
177
  // layout's idle, a chat layout's welcome). No synthetic "default" entry: the first
178
178
  // authored layer IS the default view.
179
179
  // The LAYOUT axis (multi-layout templates): the picked non-default layout pill, or
@@ -196,7 +196,7 @@ export function TemplatesView({
196
196
  useEffect(() => { if (live) { setActiveState(null); setActiveLayout(null); } }, [live]); // states are mock-only — clear when going live
197
197
  // PICKING a state = jump there and seed its preview scenario (seedKey forces a fresh
198
198
  // store). MOVING between states organically (clicking inside the preview) keeps the
199
- // store — the template simply transitions, exactly like the runtime.
199
+ // store — the app simply transitions, exactly like the runtime.
200
200
  const [seedKey, setSeedKey] = useState(0);
201
201
  const store = useMemo(() => new ComponentStore(), [live, selected, seedKey]);
202
202
  // THE HOST'S JOB: the stage wrapper's width is DERIVED from component views
@@ -208,7 +208,7 @@ export function TemplatesView({
208
208
  useCallback(() => store.getVersion(), [store]),
209
209
  );
210
210
  const [seeded, setSeeded] = useState<string[]>([]);
211
- const [metaOpen, setMetaOpen] = usePersistentState<boolean>("wb:templates:meta", false); // MCP details panel
211
+ const [metaOpen, setMetaOpen] = usePersistentState<boolean>("wb:apps:meta", false); // MCP details panel
212
212
  const [copiedUri, setCopiedUri] = useState(false);
213
213
  const [version, setVersion] = useState(0); // local bump to reflect store changes in the toolbar
214
214
  const [stageWidth, setStageWidth] = useState<number | null>(null); // default Full — testing should see the real channel width
@@ -315,10 +315,10 @@ export function TemplatesView({
315
315
  }),
316
316
  [liveBase, token],
317
317
  );
318
- // The APP decides which workflow runs: the selected template's manifest binding
318
+ // The APP decides which workflow runs: the selected app's manifest binding
319
319
  // (workflow + trigger) drives the connection. No manual entry — MCP decides.
320
320
  const selectedDef = useMemo(() => templates.find((t) => t.name === selected), [templates, selected]);
321
- // THE TEMPLATE'S ACTIVE STATE, derived live (docs/design/04: one state at a time —
321
+ // THE APP'S ACTIVE STATE, derived live (docs/design/04: one state at a time —
322
322
  // the latest surfaced view; else the conversation/welcome base). The nav highlights
323
323
  // THIS, not the picked seed: clicking a card inside the preview IS moving to the
324
324
  // other state, and Studio simply follows the same transition the runtime makes.
@@ -347,11 +347,11 @@ export function TemplatesView({
347
347
  }
348
348
  }
349
349
  if (view) return claims.get(view) ?? null;
350
- // LOCAL states carry their own selector (`when`, a condition on template state —
350
+ // LOCAL states carry their own selector (`when`, a condition on app state —
351
351
  // railState/callState-style discriminants). Evaluate it with the RENDERER'S OWN
352
- // predicate (`visible`, one condition semantics), against the template-state bag —
352
+ // predicate (`visible`, one condition semantics), against the app-state bag —
353
353
  // so a pill pick, a component's write, or a live workflow echo all light the same.
354
- const ts = store.getTemplateState() as Record<string, unknown>;
354
+ const ts = store.getAppState() as Record<string, unknown>;
355
355
  const local = states.find((s) => s.when && typeof s.when === "object" && visible(s.when as Parameters<typeof visible>[0], ts));
356
356
  if (local) return local.name;
357
357
  const base = store.getTimeline().length === 0 ? "isEmpty" : "hasMessages";
@@ -432,18 +432,18 @@ export function TemplatesView({
432
432
  [canConnect, rawConn],
433
433
  );
434
434
 
435
- // The workflow can OVERRIDE the manual pick (WORKFLOW_STARTED metadata.template →
436
- // store.activeTemplate) with a BARE name — so the org must be resolved for whichever
437
- // template is actually loading, not the picked one, or the resources/read goes out
438
- // org-less and the server refuses it ("template address needs its org").
439
- const activeName = conn.activeTemplate ?? selected;
435
+ // The workflow can OVERRIDE the manual pick (WORKFLOW_STARTED metadata.app →
436
+ // store.activeApp) with a BARE name — so the org must be resolved for whichever
437
+ // app is actually loading, not the picked one, or the resources/read goes out
438
+ // org-less and the server refuses it ("app address needs its org").
439
+ const activeName = conn.activeApp ?? selected;
440
440
  const activeDef = useMemo(
441
441
  () => (activeName === selected ? selectedDef : (templates.find((t) => t.name === activeName) ?? selectedDef)),
442
442
  [templates, activeName, selected, selectedDef],
443
443
  );
444
444
 
445
445
  // The `voice` native service (§2e-1) now lives INSIDE the SDK renderer (shared by every
446
- // host — see StreamedUnoverseTemplate). The host only DECLARES the app is voice (off its
446
+ // host — see StreamedUnoverseApp). The host only DECLARES the app is voice (off its
447
447
  // manifest `service`) and hands the renderer the connection identity it already holds;
448
448
  // the renderer owns the service + answers startCall/endCall/toggleMute. No host wiring.
449
449
  const isVoice = selectedDef?.service === "voice";
@@ -463,8 +463,8 @@ export function TemplatesView({
463
463
  workflowId: session.workflowId,
464
464
  targetTriggerNode: session.targetTriggerNode,
465
465
  },
466
- // Producer wiring (STATE_MODEL §4): the service writes its state into TEMPLATE
467
- // STATE on this store — templates read callState/isMuted/… from the global bag.
466
+ // Producer wiring (STATE_MODEL §4): the service writes its state into APP
467
+ // STATE on this store — apps read callState/isMuted/… from the global bag.
468
468
  store,
469
469
  }
470
470
  : undefined,
@@ -472,7 +472,7 @@ export function TemplatesView({
472
472
  );
473
473
 
474
474
  // "Load the app → it calls its workflow" — but ONLY for apps with autoTrigger (Mode B).
475
- // Mode A apps (autoTrigger false/absent) just load the template and wait. Fires once the
475
+ // Mode A apps (autoTrigger false/absent) just load the app and wait. Fires once the
476
476
  // live connection is ready, no user turn; resets on app/Live change so re-selecting re-runs.
477
477
  const [triggered, setTriggered] = useState(false);
478
478
  useEffect(() => setTriggered(false), [selected, live]);
@@ -544,7 +544,7 @@ export function TemplatesView({
544
544
  // ARRIVAL CONTRACT (STATE_MODEL §5b): a component streams in WITH its state already
545
545
  // set — the seed carries the def's `state` block (its initial values). EXCEPT
546
546
  // `defaultState`: in the preview the SELECTED STATE CHIP is the state controller —
547
- // seeding always lands inline (the universal default), and the template's focus
547
+ // seeding always lands inline (the universal default), and the app's focus
548
548
  // state chip writes `focused` into the slice (the where-activation below), exactly
549
549
  // what a real arrival-in-focus or a component's own expand button would do.
550
550
  function seed(comps: Def[]) {
@@ -576,7 +576,7 @@ export function TemplatesView({
576
576
  }
577
577
 
578
578
  // Which components to mock for the current selection: ONLY an app's declared
579
- // previewComponents. No declaration → seed nothing, so the template renders its own
579
+ // previewComponents. No declaration → seed nothing, so the app renders its own
580
580
  // default/welcome state (the default view) instead of a dump of the whole catalog.
581
581
  const seedSet = (tmpls: Def[], comps: Def[], sel: string | null): Def[] => {
582
582
  const t = tmpls.find((t) => t.name === sel);
@@ -593,15 +593,15 @@ export function TemplatesView({
593
593
  Promise.all([
594
594
  // The header's org scope filters the list ("all" → every org's apps, each
595
595
  // tagged); the selected app's org still picks its own preview theme.
596
- // Components are SCOPED like the templates: an org sees "universal + its own".
596
+ // Components are SCOPED like the apps: an org sees "universal + its own".
597
597
  // Fetched unscoped, a cloned org's same-named component (design/lyceum cloned
598
598
  // from design/bpp → two bare `journey-finder`s) sat beside the original and
599
599
  // Mock seeding picked whichever listed first.
600
- authedFetch(`/dev/templates${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`).then((r) => (r.ok ? r.json() : { templates: [] })),
600
+ authedFetch(`/dev/apps${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`).then((r) => (r.ok ? r.json() : { apps: [] })),
601
601
  authedFetch(`/dev/components${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`).then((r) => (r.ok ? r.json() : { components: [] })),
602
602
  ])
603
603
  .then(([t, c]) => {
604
- const tl: Def[] = t.templates ?? [];
604
+ const tl: Def[] = t.apps ?? [];
605
605
  const cl: Def[] = c.components ?? [];
606
606
  setTemplates(tl);
607
607
  setComponents(cl);
@@ -626,17 +626,17 @@ export function TemplatesView({
626
626
  if (live || !components.length || !selected) return;
627
627
  const when = activeState?.when;
628
628
  // TWO AXES compose (name-sync, docs/design/05): the LAYOUT pill picks the surfaced
629
- // view (seeds components into it) while the LOCAL state picks the template's own
629
+ // view (seeds components into it) while the LOCAL state picks the app's own
630
630
  // condition (callState, hasMessages) — any layout × any state. `activeLayout` is
631
631
  // the non-default layout pill, or null (default layout, no surfaced component).
632
632
  const layoutSel = activeLayout ? { field: "defaultState", eq: activeLayout } : undefined;
633
633
  // A picked state activates by its selector (UNOVERSE_LAYERS.md §7):
634
- // • "isEmpty" → leave the store pristine (the template's empty/welcome layer)
634
+ // • "isEmpty" → leave the store pristine (the app's empty/welcome layer)
635
635
  // • "hasMessages" / a component state / default → seed the mock conversation
636
- // • { field, eq } (template-state selector, e.g. defaultState="focus") → set it after seeding
636
+ // • { field, eq } (app-state selector, e.g. defaultState="focus") → set it after seeding
637
637
  // An explicit preview entry for the state OVERRIDES all of it — the author decided.
638
638
  const previewMap = selectedDef?.app?.preview;
639
- // An OBJECT preview entry is authored TEMPLATE-STATE mock data — what the workflow
639
+ // An OBJECT preview entry is authored APP-STATE mock data — what the workflow
640
640
  // would have echoed (comments, railState, an anchor) — merged as-is so a state's
641
641
  // pill previews with real content. Arrays keep the existing meaning: component seeds.
642
642
  const stateEntry = activeState ? previewMap?.[activeState.name] : undefined;
@@ -645,8 +645,8 @@ export function TemplatesView({
645
645
  const entry = Array.isArray(layoutEntry) ? layoutEntry : undefined;
646
646
  const wantEmpty = !entry && !activeLayout && typeof when === "string" && when === "isEmpty";
647
647
  // A COMPONENT is only seeded for a reaction-contract state (select.where) — the
648
- // surface needs one to grab (Focus). Plain template states (welcome, conversation)
649
- // preview the TEMPLATE's own chrome; a component in the transcript there is noise.
648
+ // surface needs one to grab (Focus). Plain app states (welcome, conversation)
649
+ // preview the APP's own chrome; a component in the transcript there is noise.
650
650
  const wantComponent = Boolean(layoutSel) || Boolean((activeState?.where as { field?: string } | undefined)?.field);
651
651
  let seededSet: Def[] = [];
652
652
  if (!wantEmpty) {
@@ -667,7 +667,7 @@ export function TemplatesView({
667
667
  if (set.length || activeState) {
668
668
  // An autoTrigger app launches by RAW trigger — nothing prints to the
669
669
  // conversation (protocol §launch-from-UI). The mock mirrors it: components
670
- // seed without the synthetic user turn, so a template that renders user turns
670
+ // seed without the synthetic user turn, so an app that renders user turns
671
671
  // outside a chat (a review pile) never shows "Show me this app" as content.
672
672
  if (!selectedDef?.app?.autoTrigger) store.addUserMessage(chatIdFor(selected ?? "app"), "Show me this app");
673
673
  // Component states need a body to grab; app declares no previewComponents →
@@ -685,13 +685,13 @@ export function TemplatesView({
685
685
  }
686
686
  if (when && typeof when === "object" && "field" in (when as object)) {
687
687
  const c = when as { field: string; eq?: unknown };
688
- store.mergeTemplateState({ [c.field]: c.eq ?? true });
688
+ store.mergeAppState({ [c.field]: c.eq ?? true });
689
689
  } else if (typeof when === "string" && when !== "isEmpty" && when !== "hasMessages") {
690
- store.mergeTemplateState({ [when]: true });
690
+ store.mergeAppState({ [when]: true });
691
691
  }
692
- // Authored template-state mock (object preview entry) — merged AFTER the selector so
692
+ // Authored app-state mock (object preview entry) — merged AFTER the selector so
693
693
  // the author's data wins (it may refine the same keys, e.g. railState + comments).
694
- if (stateData) store.mergeTemplateState(stateData);
694
+ if (stateData) store.mergeAppState(stateData);
695
695
  // Reaction-contract activation (STATE_MODEL §5b): the surface (or the layout pill —
696
696
  // same selector) reacts to COMPONENT state, so activate it the way the runtime
697
697
  // does — put a seeded component INTO that view. Same write the component's own
@@ -716,7 +716,7 @@ export function TemplatesView({
716
716
  <nav className={NAV}>
717
717
  <div className={NAV_SECTION}>Apps</div>
718
718
  {templates.length === 0 && (
719
- <div className="p-2 text-xs text-gray-400">No apps yet — add one under design/&lt;project&gt;/templates/.</div>
719
+ <div className="p-2 text-xs text-gray-400">No apps yet — add one under design/&lt;project&gt;/apps/.</div>
720
720
  )}
721
721
  {templates.map((t) => (
722
722
  <div key={`${t.org ?? ""}/${t.name}`}>
@@ -732,7 +732,7 @@ export function TemplatesView({
732
732
  </span>
733
733
  )}
734
734
  </button>
735
- {/* The template's LOCAL states — its states/ folder. STATE MODEL v2: these
735
+ {/* The app's LOCAL states — its states/ folder. STATE MODEL v2: these
736
736
  are PRIVATE SUBSTATES of the BASE arrangement (welcome/conversation guard
737
737
  inside main's chrome), never states of a reaction arrangement — so they
738
738
  list ONLY while the base pill (Main) is active. Reaction states get the
@@ -765,8 +765,8 @@ export function TemplatesView({
765
765
  <div className="flex min-h-0 min-w-0 flex-col">
766
766
  {/* toolbar */}
767
767
  <div className="flex items-center gap-3 border-b px-4 py-2 text-[11px]" style={{ borderColor: theme.color["border.subtle"], color: theme.color["text.secondary"], background: theme.color["surface.base"] }}>
768
- <span className="font-medium uppercase tracking-wider opacity-60">{selected ?? "template"}</span>
769
- {/* TEMPLATE LAYOUTS (name-sync) — the faces pill row, template tier: the default
768
+ <span className="font-medium uppercase tracking-wider opacity-60">{selected ?? "app"}</span>
769
+ {/* APP LAYOUTS (name-sync) — the faces pill row, app tier: the default
770
770
  layout first, then each named layout. Picking one runs the SAME activation a
771
771
  reaction surface uses (its served `where` selector seeds a component into the
772
772
  view of that name), so the preview machinery needs zero special-casing. */}
@@ -948,7 +948,7 @@ export function TemplatesView({
948
948
  <span className="tabular-nums" style={{ color: theme.color["text.tertiary"] }}>{stageWidth ? `${stageWidth}px` : "100%"}</span>
949
949
  </div>
950
950
 
951
- {/* MCP APP DETAILS — the selected template's manifest (discovery meta + binding). */}
951
+ {/* MCP APP DETAILS — the selected app's manifest (discovery meta + binding). */}
952
952
  {metaOpen && selected && (() => {
953
953
  const app = selectedDef?.app;
954
954
  const accent = theme.color["action.primary"] ?? "#6366f1";
@@ -964,7 +964,7 @@ export function TemplatesView({
964
964
  { label: "Trigger", value: app.binding?.trigger || "—", mono: true },
965
965
  { label: "Auto-trigger", value: app.autoTrigger ? "On load" : "On message", dot: app.autoTrigger ? ok : muted },
966
966
  { label: "Layout", value: selected, mono: true },
967
- { label: "Default state", value: (() => { const v = app.defaultState ?? app.mode ?? "template"; return v.charAt(0).toUpperCase() + v.slice(1); })(), dot: (app.defaultState ?? app.mode ?? "template") === "template" ? ok : muted },
967
+ { label: "Default state", value: (() => { const v = app.defaultState ?? app.mode ?? "app"; return v.charAt(0).toUpperCase() + v.slice(1); })(), dot: (app.defaultState ?? app.mode ?? "app") === "app" ? ok : muted },
968
968
  { label: "Input params", value: Object.keys(((app.inputSchema as { properties?: Record<string, unknown> })?.properties) ?? {}).join(", ") || "—", mono: true },
969
969
  ]
970
970
  : [];
@@ -1038,7 +1038,7 @@ export function TemplatesView({
1038
1038
  {initials}
1039
1039
  </div>
1040
1040
  <div className="text-[13px] leading-relaxed" style={{ color: sub }}>
1041
- <span className="font-semibold" style={{ color: theme.color["text.primary"] }}>Plain template</span> — not yet a discoverable MCP app.
1041
+ <span className="font-semibold" style={{ color: theme.color["text.primary"] }}>Plain app</span> — not yet a discoverable MCP app.
1042
1042
  <br />Add a <code className="rounded px-1 font-mono text-[11px]" style={{ background: theme.color["surface.sunken"] }}>manifest.json</code> beside{" "}
1043
1043
  <code className="rounded px-1 font-mono text-[11px]" style={{ background: theme.color["surface.sunken"] }}>{selected.toLowerCase()}.json</code> to give it a name, description, and workflow binding.
1044
1044
  </div>
@@ -1048,13 +1048,13 @@ export function TemplatesView({
1048
1048
  );
1049
1049
  })()}
1050
1050
 
1051
- {/* STAGE — templates are full-screen layouts; render in a centered device frame */}
1051
+ {/* STAGE — apps are full-screen layouts; render in a centered device frame */}
1052
1052
  <section className="flex min-h-0 flex-1 justify-center overflow-auto p-6 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden" style={{ background: theme.color["surface.sunken"] ?? "#f3f4f6" }} data-store-version={version} data-store-width={storeVersion}>
1053
1053
  {selected && (
1054
1054
  <div
1055
- // The host just provides the DEVICE BOX; the template's own layout DATA fills it
1056
- // (a template surface fills its container — see StreamedUnoverseTemplate). Per-
1057
- // template height mode (manifest `fluidHeight`, default true): FLUID gives a
1055
+ // The host just provides the DEVICE BOX; the app's own layout DATA fills it
1056
+ // (an app surface fills its container — see StreamedUnoverseApp). Per-
1057
+ // app height mode (manifest `fluidHeight`, default true): FLUID gives a
1058
1058
  // full-height box (chat/voice fill it); CONTENT gives an auto-height box that
1059
1059
  // wraps the rendered component (widget apps like Bank Transfer) — `self-start`
1060
1060
  // so the flex stage doesn't stretch it.
@@ -1098,26 +1098,26 @@ export function TemplatesView({
1098
1098
  <NativeAppHost
1099
1099
  server={UNOVERSE_BASE || liveBase}
1100
1100
  token={token}
1101
- templateId={`${selectedDef?.org ? `${selectedDef.org}/` : ""}${selected ?? ""}`}
1101
+ appId={`${selectedDef?.org ? `${selectedDef.org}/` : ""}${selected ?? ""}`}
1102
1102
  userId={session.userId}
1103
1103
  conversationId={session.conversationId}
1104
1104
  onStatus={setNativeStatus}
1105
1105
  />
1106
1106
  ) : !activeDef && templates.length === 0 ? (
1107
- /* The workflow can announce its template BEFORE /dev/templates answers.
1107
+ /* The workflow can announce its app BEFORE /dev/apps answers.
1108
1108
  Reading it org-less is a guaranteed server refusal, so hold one beat
1109
1109
  until the list arrives and the org can be resolved. */
1110
1110
  null
1111
1111
  ) : (
1112
- <StreamedUnoverseTemplate
1112
+ <StreamedUnoverseApp
1113
1113
  client={client}
1114
1114
  store={store}
1115
- // The workflow can SELECT the template (WORKFLOW_STARTED metadata.template →
1116
- // store.activeTemplate); that overrides the manual pick. Loaded via the
1117
- // official resources/read unoverse://templates/{name} (templates = MCP apps).
1118
- uri={`unoverse://templates/${activeDef?.org ? `${activeDef.org}/` : ""}${activeName}`}
1115
+ // The workflow can SELECT the app (WORKFLOW_STARTED metadata.app →
1116
+ // store.activeApp); that overrides the manual pick. Loaded via the
1117
+ // official resources/read unoverse://apps/{org}/{name}/definition.
1118
+ uri={`unoverse://apps/${activeDef?.org ? `${activeDef.org}/` : ""}${activeName}/definition`}
1119
1119
  theme={theme}
1120
- // Voice state arrives via TEMPLATE STATE (the service is a producer inside
1120
+ // Voice state arrives via APP STATE (the service is a producer inside
1121
1121
  // the renderer); startCall/endCall/toggleMute are answered there too.
1122
1122
  props={undefined}
1123
1123
  voice={voiceConfig}
@@ -1179,11 +1179,11 @@ export function TemplatesView({
1179
1179
  console.error("[workbench] app launch: no binding for", appRef);
1180
1180
  return;
1181
1181
  }
1182
- // Write the app's declared default state (open NAME) into template state
1183
- // on load — templates branch on `defaultState` by name. "template" apps
1184
- // ARE the surface, so there's nothing to write.
1185
- const declared = target?.app?.defaultState ?? target?.app?.mode ?? "template";
1186
- if (declared !== "template") store.mergeTemplateState({ defaultState: declared });
1182
+ // Write the app's declared default state (open NAME) into app state
1183
+ // on load — apps branch on `defaultState` by name. An "app" app IS the
1184
+ // surface, so there's nothing to write.
1185
+ const declared = target?.app?.defaultState ?? target?.app?.mode ?? "app";
1186
+ if (declared !== "app") store.mergeAppState({ defaultState: declared });
1187
1187
  if (live) conn.trigger({ app: appRef, metadata: { targetTriggerNode: b.trigger } });
1188
1188
  else console.warn("[workbench] app launch is LIVE-only (mock has no workflow):", appRef);
1189
1189
  return;
package/screens/chrome.ts CHANGED
@@ -42,7 +42,7 @@ export const NAV_SECTION = "px-2 py-1 text-[10px] font-semibold uppercase tracki
42
42
  * SELECTION STATE ONLY, never layout.
43
43
  *
44
44
  * The navigators legitimately differ in row geometry — a Skills row is two lines, a
45
- * Templates row is a flex row with a chip, a Components row opens a state tree — and
45
+ * Apps row is a flex row with a chip, a Components row opens a state tree — and
46
46
  * flattening that into one class string would change row heights across five screens to
47
47
  * buy nothing. What must NOT differ is what "selected" looks like, and that is what these
48
48
  * return. Call sites keep their own layout classes and append one of these.
package/screens/states.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Layer resolution + state activation — shared by the Templates and Components views.
2
+ * Layer resolution + state activation — shared by the Apps and Components views.
3
3
  *
4
4
  * A component's `states/` files are its Switch-case layers: the state NAME is the
5
5
  * case key, and the Switch's `on` field is the discriminant that selects it
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * SCOPE lives here too (UNOVERSE_LAYERS.md §3c): a layer belongs to the arrangement
10
10
  * that DRAWS it — a face's steps are the Switches on the component's own keys inside
11
- * that face; a template state belongs to the layouts whose trees carry its selector.
11
+ * that face; an app state belongs to the layouts whose trees carry its selector.
12
12
  * The structure is the declaration; both views derive, neither lists.
13
13
  */
14
14
 
@@ -100,7 +100,7 @@ export function discriminantFor(root: unknown, stateName: string): string | null
100
100
  export function stateActivation(root: unknown, state: { name: string; when?: unknown }): Record<string, unknown> | null {
101
101
  const disc = discriminantFor(root, state.name);
102
102
  if (disc) return { [disc]: state.name };
103
- // Fallback: a state file that carries its own `visibleWhen` selector (template-style).
103
+ // Fallback: a state file that carries its own `visibleWhen` selector (app-style).
104
104
  const w = state.when;
105
105
  if (w && typeof w === "object" && typeof (w as { field?: unknown }).field === "string") {
106
106
  const c = w as { field: string; eq?: unknown };
package/src/App.tsx CHANGED
@@ -23,17 +23,27 @@ import { NewProject } from "./NewProject";
23
23
  // Local-only screens. Nodes are AUTHORED and TESTED here and nowhere else: Canvas keeps
24
24
  // the node library (using nodes), the marketplace acquires them (a universe's job).
25
25
  import { NodesView } from "./NodesView";
26
+ import {
27
+ Atom,
28
+ Boxes,
29
+ LayoutDashboard,
30
+ Palette,
31
+ Sparkles,
32
+ SquareDashed,
33
+ TextQuote,
34
+ type LucideIcon,
35
+ } from "lucide-react";
26
36
 
27
37
  type View = "apps" | "components" | "atoms" | "tokens" | "skills" | "prompts" | "nodes";
28
38
 
29
- const NAV: { key: View; label: string; group: string }[] = [
30
- { key: "apps", label: "Apps", group: "Build" },
31
- { key: "components", label: "Components", group: "Build" },
32
- { key: "atoms", label: "Atoms", group: "Build" },
33
- { key: "tokens", label: "Styles", group: "Build" },
34
- { key: "skills", label: "Skills", group: "AI" },
35
- { key: "prompts", label: "Prompt Blocks", group: "AI" },
36
- { key: "nodes", label: "Nodes", group: "Code" },
39
+ const NAV: { key: View; label: string; group: string; Icon: LucideIcon }[] = [
40
+ { key: "apps", label: "Apps", group: "Build", Icon: LayoutDashboard },
41
+ { key: "components", label: "Components", group: "Build", Icon: SquareDashed },
42
+ { key: "atoms", label: "Atoms", group: "Build", Icon: Atom },
43
+ { key: "tokens", label: "Styles", group: "Build", Icon: Palette },
44
+ { key: "skills", label: "Skills", group: "AI", Icon: Sparkles },
45
+ { key: "prompts", label: "Prompt Blocks", group: "AI", Icon: TextQuote },
46
+ { key: "nodes", label: "Nodes", group: "Code", Icon: Boxes },
37
47
  ];
38
48
 
39
49
  /**
@@ -112,10 +122,11 @@ function Shell() {
112
122
  <button
113
123
  key={n.key}
114
124
  onClick={() => setView(n.key)}
115
- className={`rounded px-2.5 py-1 text-xs font-medium transition ${
125
+ className={`flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition ${
116
126
  view === n.key ? "bg-white/10 text-gray-100" : "text-gray-400 hover:text-gray-200"
117
127
  }`}
118
128
  >
129
+ <n.Icon className="size-3.5" strokeWidth={1.75} aria-hidden />
119
130
  {n.label}
120
131
  </button>
121
132
  ))}
@@ -51,7 +51,7 @@ export function NewProject({
51
51
  >
52
52
  <h2 className="text-base font-semibold text-gray-100">New project</h2>
53
53
  <p className="mt-1 text-xs text-gray-500">
54
- Creates design/&lt;name&gt;/ with components, styles and templates, and one component to
54
+ Creates design/&lt;name&gt;/ with components, styles and apps, and one component to
55
55
  open on.
56
56
  </p>
57
57
 
package/src/host.ts CHANGED
@@ -28,7 +28,7 @@ export const hasAuth = false;
28
28
  * the connect button. That chrome is gone (LOCAL_STUDIO.md §Publish is terminal-only) —
29
29
  * and reading the stale localStorage value after removing the UI left Live mode aimed
30
30
  * at a universe nobody could change or clear, failing with that universe's errors
31
- * (seen live: "Unknown Unoverse template" from a universe the app was never published
31
+ * (seen live: "Unknown Unoverse app" from a universe the app was never published
32
32
  * to). So local Studio's Live preview targets the local dev platform, full stop.
33
33
  * Pointing Live at the workspace's own universe (unoverse.yaml) is future work and
34
34
  * will come from the workspace, not from a browser store.
@@ -73,7 +73,7 @@ class LocalUnoverseClient {
73
73
  * (`template.tsx` → `client.serverOrigin.replace(/^http/, "ws")`). This class is a
74
74
  * duck-typed stand-in for that client, so a member it does not implement is not a missing
75
75
  * feature, it is a CRASH at render: reading `.replace` of undefined took the whole
76
- * Studio page down the moment a template carrying a voice config was previewed.
76
+ * Studio page down the moment an app carrying a voice config was previewed.
77
77
  *
78
78
  * Studio's own origin is the honest answer. Nothing is listening on `/ws/gravity` here,
79
79
  * so voice fails when it tries to CONNECT, which is a message about needing a universe