@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
@@ -13,7 +13,7 @@ import { postToEmbedHost } from "./mcpAppBridge";
13
13
  import {
14
14
  UnoverseClient,
15
15
  ComponentStore,
16
- StreamedUnoverseTemplate,
16
+ StreamedUnoverseApp,
17
17
  useUnoverseConnection,
18
18
  useUnoverseTheme,
19
19
  computeAppWidth,
@@ -28,7 +28,7 @@ import {
28
28
  export interface AppConfig {
29
29
  serverUrl: string;
30
30
  apiUrl?: string;
31
- templateId: string;
31
+ appId: string;
32
32
  getAccessToken?: () => Promise<string | null> | string | null;
33
33
  userId?: string;
34
34
  conversationId: string;
@@ -42,7 +42,7 @@ export interface AppConfig {
42
42
  }
43
43
 
44
44
  export function AppEngine(cfg: AppConfig) {
45
- const { serverUrl, apiUrl, templateId, getAccessToken, userId, conversationId, surface, analyticsDebug } = cfg;
45
+ const { serverUrl, apiUrl, appId, getAccessToken, userId, conversationId, surface, analyticsDebug } = cfg;
46
46
 
47
47
  // ONE STORE PER CONVERSATION. The host may reuse this engine (the native-host
48
48
  // iframe posts new config on app switch), and a template swap keeps the store by
@@ -63,9 +63,9 @@ export function AppEngine(cfg: AppConfig) {
63
63
  // Setting state during render makes React re-render BEFORE the commit, so the
64
64
  // connection effect only ever sees either the old app intact or an empty binding —
65
65
  // never the cross-app hybrid.
66
- const [boundFor, setBoundFor] = useState(templateId);
67
- if (boundFor !== templateId) {
68
- setBoundFor(templateId);
66
+ const [boundFor, setBoundFor] = useState(appId);
67
+ if (boundFor !== appId) {
68
+ setBoundFor(appId);
69
69
  setBinding({});
70
70
  }
71
71
  // STATE-OWNED WIDTH (design data): the layout root's `appWidth` = the core surface's
@@ -81,11 +81,11 @@ export function AppEngine(cfg: AppConfig) {
81
81
  const [def, setDef] = useState<UnoverseDefinition | null>(null);
82
82
  const lastWidthRef = useRef<string | undefined>(undefined);
83
83
  // Named app sizes resolve via the served theme (theme.appSize), like every token.
84
- // The theme's name is the template's org. When templateId already carries it
84
+ // The theme's name is the template's org. When appId already carries it
85
85
  // ("org/name" — every production boot), start the theme read IMMEDIATELY, in
86
86
  // parallel with the template read; waiting for the definition just to learn an org
87
87
  // we were handed serialized two round trips for nothing. Bare ids (dev) still wait.
88
- const orgFromId = templateId.includes("/") ? templateId.split("/")[0] : null;
88
+ const orgFromId = appId.includes("/") ? appId.split("/")[0] : null;
89
89
  const theme = useUnoverseTheme(
90
90
  client,
91
91
  orgFromId ? `${orgFromId}/light` : def ? (def.org ? `${def.org}/light` : "light") : null,
@@ -99,7 +99,7 @@ export function AppEngine(cfg: AppConfig) {
99
99
  // the previous app's trigger within the same commit.
100
100
  client
101
101
  .connect()
102
- .then(() => (client as any).client.readResource({ uri: `unoverse://apps/${templateId.toLowerCase()}` }))
102
+ .then(() => (client as any).client.readResource({ uri: `unoverse://apps/${appId.toLowerCase()}` }))
103
103
  .then((res: any) => {
104
104
  const parsed = res?.contents?.[0]?.text ? JSON.parse(res.contents[0].text) : null;
105
105
  const m = parsed?.manifest ?? parsed ?? {};
@@ -113,7 +113,7 @@ export function AppEngine(cfg: AppConfig) {
113
113
  // definition too — same resource the renderer loads.
114
114
  client
115
115
  .connect()
116
- .then(() => client.readDefinition(`unoverse://templates/${templateId}`))
116
+ .then(() => client.readDefinition(`unoverse://apps/${appId}/definition`))
117
117
  .then((d) => {
118
118
  if (alive) setDef(d ?? null);
119
119
  })
@@ -121,7 +121,7 @@ export function AppEngine(cfg: AppConfig) {
121
121
  return () => {
122
122
  alive = false;
123
123
  };
124
- }, [client, templateId]);
124
+ }, [client, appId]);
125
125
 
126
126
  // Report the app's width UP to the embedding host (iframe boundary → the twin of the
127
127
  // inbound `unoverse:config` channel), re-reporting whenever a panel surface opens or
@@ -145,13 +145,13 @@ export function AppEngine(cfg: AppConfig) {
145
145
  useEffect(() => {
146
146
  // A NEW APP RE-ANNOUNCES ITS WIDTH, ALWAYS. The native host keeps ONE iframe for every
147
147
  // app (its srcdoc is keyed on the server, not the app — it just re-posts
148
- // `unoverse:config` with a new templateId) and this engine is re-rendered in place, so
148
+ // `unoverse:config` with a new appId) and this engine is re-rendered in place, so
149
149
  // the change-guard below outlives the switch — while the host RESETS its frame to the
150
150
  // default on it (NativeAppHost/TemplatesView). Two apps of the same declared width is
151
151
  // the ordinary case (one org, one `chat` size), and the incoming app then reported
152
152
  // nothing: its frame sat at the host default (100%) while its core panel held its
153
153
  // declared width, leaving a dead band beside it (observed live 2026-08-13,
154
- // bpp-voice-layout). Clearing the guard here makes the first report after an app
154
+ // bpp-voice). Clearing the guard here makes the first report after an app
155
155
  // switch unconditional; the guard still suppresses repeats within one app.
156
156
  lastWidthRef.current = undefined;
157
157
  const report = () => {
@@ -208,7 +208,7 @@ export function AppEngine(cfg: AppConfig) {
208
208
  }, [conn.isReady, conn.isConnected, conn.refusal, failure]);
209
209
 
210
210
  // VOICE (§2e-1): a `service: "voice"` app runs the native voice service — which now lives
211
- // in the shared renderer (StreamedUnoverseTemplate), identical to Studio. The host only
211
+ // in the shared renderer (StreamedUnoverseApp), identical to Studio. The host only
212
212
  // supplies the connection identity it already holds; startCall/endCall/toggleMute are
213
213
  // answered inside the renderer, so this host wires NO voice buttons. Absent → inert.
214
214
  const voiceConfig = useMemo<UseVoiceServiceConfig | undefined>(
@@ -235,10 +235,10 @@ export function AppEngine(cfg: AppConfig) {
235
235
  );
236
236
 
237
237
  return (
238
- <StreamedUnoverseTemplate
238
+ <StreamedUnoverseApp
239
239
  client={client}
240
240
  store={store}
241
- uri={`unoverse://templates/${templateId}`}
241
+ uri={`unoverse://apps/${appId}/definition`}
242
242
  voice={voiceConfig}
243
243
  onAction={(action, data, meta) => {
244
244
  const t = typeof action === "string" ? action : (action as any)?.type;
@@ -270,10 +270,10 @@ export function AppEngine(cfg: AppConfig) {
270
270
  // pre-rename alias) into template state NOW — e.g. a "focus" app pre-opens
271
271
  // the focus surface, whose slot shows its fallback (skeleton) until the
272
272
  // workflow streams the component in. Same template-state key the node's
273
- // TEMPLATE_DATA emit writes; no new vocabulary. "template" apps ARE the
274
- // surface — nothing to write.
273
+ // APP_DATA emit writes; no new vocabulary. An "app" app IS the surface
274
+ // — nothing to write.
275
275
  const declared = parsed?.manifest?.defaultState ?? parsed?.manifest?.mode ?? parsed?.defaultState ?? parsed?.mode;
276
- if (declared && declared !== "template") store.mergeTemplateState({ defaultState: declared });
276
+ if (declared && declared !== "app") store.mergeAppState({ defaultState: declared });
277
277
  conn.trigger({ app, metadata: { targetTriggerNode: b.trigger } });
278
278
  })
279
279
  .catch((e: any) => console.error("[app] launch failed:", e));
@@ -1,5 +1,5 @@
1
1
  /**
2
- * <StreamedUnoverseTemplate> — renders a TEMPLATE definition (the layout) and
2
+ * <StreamedUnoverseApp> — renders a TEMPLATE definition (the layout) and
3
3
  * fills its ComponentSlots from the store's timeline.
4
4
  *
5
5
  * A template owns nothing (UNOVERSE_SPEC.md §2e-0): it reads the shared state
@@ -34,7 +34,7 @@ import {
34
34
  } from "./core";
35
35
  import { renderNode, styleToCss, keyframesCss, type ActionHandler, type SlotResolver } from "./render";
36
36
 
37
- // The pure template derivations moved to core/templates.ts (the portable brain —
37
+ // The pure template derivations moved to core/apps.ts (the portable brain —
38
38
  // framework-agnostic, guarded by sdk-core-portable.test.ts). Re-exported here so
39
39
  // existing consumers keep their import path.
40
40
  export { collectSurfacedViews, computeAppWidth, orderedClaims, resolveActiveLayout, resolveActiveView, selectPointers, viewOf } from "./core";
@@ -43,10 +43,10 @@ import { useUnoverseTheme, ensureFontStylesheets } from "./theme";
43
43
  import { IsolatedRoot } from "./isolate";
44
44
  import { useVoiceService, type UseVoiceServiceConfig } from "./voice";
45
45
 
46
- export interface StreamedUnoverseTemplateProps {
46
+ export interface StreamedUnoverseAppProps {
47
47
  client: UnoverseClient;
48
48
  store: ComponentStore;
49
- /** e.g. "unoverse://templates/KeyService" */
49
+ /** e.g. "unoverse://apps/acme/KeyService/definition" */
50
50
  uri: string;
51
51
  onAction?: ActionHandler;
52
52
  theme?: ResolvedTheme;
@@ -72,7 +72,7 @@ const INERT_VOICE: UseVoiceServiceConfig = {
72
72
  session: { userId: "", conversationId: "", workflowId: "", targetTriggerNode: "" },
73
73
  };
74
74
 
75
- export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme: themeProp, props, isolate = true, voice: voiceConfig }: StreamedUnoverseTemplateProps) {
75
+ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: themeProp, props, isolate = true, voice: voiceConfig }: StreamedUnoverseAppProps) {
76
76
  const [def, setDef] = useState<UnoverseDefinition | null>(null);
77
77
  const [error, setError] = useState<string | null>(null);
78
78
  // The voice native service lives HERE (shared), not in each host. Called every render
@@ -189,14 +189,14 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
189
189
  * dies with the thing dismissed: a different component taking the latch is a different
190
190
  * key, and its pill comes up as it should rather than being suppressed by a stale flag.
191
191
  */
192
- const tstate = store.getTemplateState() as Record<string, unknown>;
192
+ const tstate = store.getAppState() as Record<string, unknown>;
193
193
  const dismissedRaw = tstate.latchDismissed;
194
194
  const held = def ? resolveLatch(def, store, typeof dismissedRaw === "string" ? dismissedRaw : undefined) : null;
195
195
  useEffect(() => {
196
196
  if (dismissedRaw === true) {
197
197
  // Resolve the bare flag against whoever holds it RIGHT NOW, then re-derive.
198
198
  const current = def ? resolveLatch(def, store) : null;
199
- store.mergeTemplateState({ latchDismissed: current ? current.key : "" });
199
+ store.mergeAppState({ latchDismissed: current ? current.key : "" });
200
200
  return;
201
201
  }
202
202
  const l = held?.latch as { title?: unknown } | undefined;
@@ -208,7 +208,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
208
208
  // its condition — which is why they are separate keys and not the condition itself.
209
209
  const latched = held != null;
210
210
  if (tstate.latched !== latched || tstate.latchTitle !== title || tstate.latchKey !== (held?.key ?? ""))
211
- store.mergeTemplateState({ latched, latchTitle: title, latchKey: held?.key ?? "" });
211
+ store.mergeAppState({ latched, latchTitle: title, latchKey: held?.key ?? "" });
212
212
  }, [
213
213
  held?.key,
214
214
  held?.latch,
@@ -226,7 +226,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
226
226
  if (!def || !theme) return <div>Loading {uri}…</div>;
227
227
 
228
228
  // ONE action interpreter for the whole tree: template chrome runs the same
229
- // `dispatchAction` (core/actions.ts) every leaf runs — `setTemplateValue` writes
229
+ // `dispatchAction` (core/actions.ts) every leaf runs — `setAppValue` writes
230
230
  // template state, `then` chains, anything else routes to the channel. Template
231
231
  // chrome has NO component slice, so `setValue` here is an authoring error (it
232
232
  // belongs on the component); the empty ids make that write inert.
@@ -321,7 +321,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
321
321
  const frame = appWidth
322
322
  ? ({ ...node.frame, style: { ...(node.frame.style ?? {}), width: appWidth, flex: "0 0 auto" } } as UnoverseNode)
323
323
  : node.frame;
324
- return renderNode(frame, store.getTemplateState() as Record<string, unknown>, dispatch, theme, key ?? "frame", innerSlot, def?.defs);
324
+ return renderNode(frame, store.getAppState() as Record<string, unknown>, dispatch, theme, key ?? "frame", innerSlot, def?.defs);
325
325
  }
326
326
  return pointers.map((p) => leaf(p));
327
327
  };
@@ -404,7 +404,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
404
404
  // The active template's OWN state — one generic bag of the DEV's chosen keys (draft,
405
405
  // openPanel, suggestions data, focusMode, voice call state, …). No per-feature
406
406
  // projections; the engine knows no key names (UNOVERSE_STATE_MODEL §2).
407
- ...store.getTemplateState(),
407
+ ...store.getAppState(),
408
408
  // Conversation-derived facts — READ OFF the timeline / lifecycle, never stored.
409
409
  isEmpty,
410
410
  hasMessages: !isEmpty,
@@ -6,7 +6,7 @@
6
6
  * sending messages. Inbound `notifications/unoverse/event` messages feed the single
7
7
  * `ComponentStore` via the pure `applyServerMessage` mapper.
8
8
  *
9
- * Per UNOVERSE_MCP_TEMPLATE_PROTOCOL.md §5/§5a/§5b: the component stream rides MCP
9
+ * Per UNOVERSE_MCP_APP_PROTOCOL.md §5/§5a/§5b: the component stream rides MCP
10
10
  * (NOT the legacy `/ws/gravity` WS — that's reserved for live audio). The node
11
11
  * executes in the same Unoverse process that serves this stream, so there's no
12
12
  * gateway relay and no Redis in the component path.
@@ -65,8 +65,8 @@ export interface UnoverseConnection {
65
65
  * channel then falls back to `sendAction`). */
66
66
  resolveElicitation: (state: Record<string, unknown>) => boolean;
67
67
  /** The workflow-selected template (MCP app) name, or null. The channel renders
68
- * `unoverse://templates/{activeTemplate}` when set — the official resources/read load. */
69
- activeTemplate: string | null;
68
+ * `unoverse://apps/{activeApp}/definition` when set — the official resources/read load. */
69
+ activeApp: string | null;
70
70
  }
71
71
 
72
72
  /** The data-plane endpoint on the Unoverse server (§5b). */
@@ -171,7 +171,7 @@ export function useUnoverseConnection(
171
171
  const connect = async () => {
172
172
  if (cancelled) return;
173
173
  // Declare the ELICITATION capability (form mode) — the native collection path
174
- // (UNOVERSE_MCP_TEMPLATE_PROTOCOL §3.3): the server pauses inside an app tool
174
+ // (UNOVERSE_MCP_APP_PROTOCOL §3.3): the server pauses inside an app tool
175
175
  // call and asks THIS session for the user's structured input. The interactive
176
176
  // component already on screen IS the form; its `submit` answers the elicitation.
177
177
  const client = new Client(
@@ -317,7 +317,7 @@ export function useUnoverseConnection(
317
317
  const turn = turnId ?? mintTurn();
318
318
  (async () => {
319
319
  // NATIVE MCP: fire the run via a `tools/call` on the live /stream session — NOT
320
- // the retired REST `/api/workflows/:id/execute` (UNOVERSE_MCP_TEMPLATE_PROTOCOL
320
+ // the retired REST `/api/workflows/:id/execute` (UNOVERSE_MCP_APP_PROTOCOL
321
321
  // §0.3). Fire-and-forget: the run's UI streams back over this same session; the
322
322
  // caller's token rides the connection (server reads it from extra.authInfo).
323
323
  const client = clientRef.current;
@@ -378,8 +378,8 @@ export function useUnoverseConnection(
378
378
  const cache = clientRef.current as unknown as
379
379
  | { peekDefinition?: (uri: string) => Parameters<typeof resolveLatch>[0] }
380
380
  | null;
381
- const tmpl = store.getActiveTemplate().name;
382
- const def = tmpl ? (cache?.peekDefinition?.(`unoverse://templates/${tmpl}`) ?? null) : null;
381
+ const tmpl = store.getActiveApp().name;
382
+ const def = tmpl ? (cache?.peekDefinition?.(`unoverse://apps/${tmpl}/definition`) ?? null) : null;
383
383
  const held = def ? resolveLatch(def, store) : null;
384
384
  trigger(
385
385
  held
@@ -412,9 +412,9 @@ export function useUnoverseConnection(
412
412
  // The workflow-selected template, reactively (a primitive snapshot so the store
413
413
  // subscription doesn't loop). The channel renders it via resources/read.
414
414
  // Stable subscribe fn — an inline arrow re-subscribes every render.
415
- const activeTemplate = useSyncExternalStore(
415
+ const activeApp = useSyncExternalStore(
416
416
  useCallback((cb) => store.subscribe(cb), [store]),
417
- useCallback(() => store.getActiveTemplate().name, [store]),
417
+ useCallback(() => store.getActiveApp().name, [store]),
418
418
  );
419
419
 
420
420
  // Answer a pending elicitation with the submitted component state (native path).
@@ -425,5 +425,5 @@ export function useUnoverseConnection(
425
425
  return true;
426
426
  }, []);
427
427
 
428
- return { isConnected, isReady, refusal, sendMessage, trigger, sendAction, resolveElicitation, activeTemplate };
428
+ return { isConnected, isReady, refusal, sendMessage, trigger, sendAction, resolveElicitation, activeApp };
429
429
  }
@@ -73,14 +73,14 @@ export function dispatchAction(
73
73
  const entered = patch.view ?? patch.defaultState;
74
74
  ctx.sendToServer?.("viewEntered", { layout: entered, view: entered });
75
75
  }
76
- } else if (spec.type === "setTemplateValue") {
76
+ } else if (spec.type === "setAppValue") {
77
77
  // Write the dev's chosen keys into TEMPLATE state — the template-state twin of setValue.
78
78
  // No component scope. The dev builds disclosure / focus / etc. from a key THEY name
79
79
  // (openPanel, focusMode, …) and reads it via `visibleWhen`. The SDK hardcodes NO UI
80
80
  // concept — only "write a key to component state" and "write a key to template state".
81
81
  const patch: Record<string, unknown> = {};
82
82
  for (const { key, value } of spec.values ?? []) patch[key] = resolveValue(value, scope);
83
- ctx.store.mergeTemplateState(patch);
83
+ ctx.store.mergeAppState(patch);
84
84
  } else if (spec.type === "input") {
85
85
  // A controlled Input changed — write its bound field back into the state bag that
86
86
  // OWNS this tree (local two-way binding; every keystroke is local, never a server
@@ -92,7 +92,7 @@ export function dispatchAction(
92
92
  const field = typeof scope.field === "string" ? scope.field : undefined;
93
93
  if (field) {
94
94
  if (ctx.nodeId) ctx.store.apply({ type: "COMPONENT_DATA", chatId: ctx.chatId, nodeId: ctx.nodeId, data: { [field]: scope.text } });
95
- else ctx.store.mergeTemplateState({ [field]: scope.text });
95
+ else ctx.store.mergeAppState({ [field]: scope.text });
96
96
  }
97
97
  } else {
98
98
  // SERVER — narrow the payload by `send` if given, else forward the whole scope.
@@ -4,7 +4,7 @@
4
4
  * Everything a template "decides" is DERIVED here from the store, never stored:
5
5
  * the active layout (name-sync), the active view (with the pinned/guest-control
6
6
  * law), which pointers a reaction surface selects, which views are claimed, and
7
- * the app's declared width. `StreamedUnoverseTemplate` (the React binding) calls
7
+ * the app's declared width. `StreamedUnoverseApp` (the React binding) calls
8
8
  * these; a platform port (Flutter/Swift/Kotlin) re-implements its own binding and
9
9
  * calls the SAME derivations — this file IS the port list for the template tier.
10
10
  *
@@ -7,7 +7,7 @@
7
7
  * React hook (react/src/connection.tsx) own only socket lifecycle.
8
8
  *
9
9
  * The Unoverse MCP server is NOT involved here — it's the control plane
10
- * (definitions + theme). See UNOVERSE_MCP_TEMPLATE_PROTOCOL.md §5a.
10
+ * (definitions + theme). See UNOVERSE_MCP_APP_PROTOCOL.md §5a.
11
11
  */
12
12
  import type { ComponentStore } from "./store";
13
13
 
@@ -23,7 +23,7 @@ export interface ServerComponentInit {
23
23
  /** Marks a TEMPLATE directive (legacy emits a fake COMPONENT_INIT with nodeId
24
24
  * `<name>_template`). Templates are SERVED apps, loaded via resources/read — never
25
25
  * rendered as components — so we route these to the template plane, not the timeline. */
26
- metadata?: { isTemplate?: boolean; [k: string]: unknown };
26
+ metadata?: { isApp?: boolean; [k: string]: unknown };
27
27
  }
28
28
  export interface ServerComponentData {
29
29
  type: "COMPONENT_DATA" | "OBJECT_DATA";
@@ -37,17 +37,19 @@ export interface ServerWorkflowState {
37
37
  chatId: string;
38
38
  workflowId?: string;
39
39
  workflowRunId?: string | null;
40
- /** Template SELECTION rides here (SPEC §5b: `{ template, templateMode }`) — the
41
- * OFFICIAL signal for which MCP-app shell to load via resources/read. */
42
- metadata?: { template?: string; templateMode?: string; [k: string]: unknown };
40
+ /** APP SELECTION rides here (SPEC §5b: `{ app, appMode }`) — the OFFICIAL signal for
41
+ * which MCP-app shell to load via resources/read. Note this `app` is a bare app NAME.
42
+ * A content ROW's `metadata.app` is a different bag entirely and holds a component URI;
43
+ * the two never meet on one object. */
44
+ metadata?: { app?: string; appMode?: string; [k: string]: unknown };
43
45
  }
44
46
  export interface ServerSessionReady {
45
47
  type: "SESSION_READY";
46
48
  }
47
49
  /** Generic write into TEMPLATE STATE — the template-state twin of COMPONENT_DATA. The
48
50
  * producer names the keys (e.g. the Suggestions node sends `{ faqs }`); core stays generic. */
49
- export interface ServerTemplateData {
50
- type: "TEMPLATE_DATA";
51
+ export interface ServerAppData {
52
+ type: "APP_DATA";
51
53
  data: Record<string, unknown>;
52
54
  }
53
55
  export type ServerMessage =
@@ -55,7 +57,7 @@ export type ServerMessage =
55
57
  | ServerComponentData
56
58
  | ServerWorkflowState
57
59
  | ServerSessionReady
58
- | ServerTemplateData
60
+ | ServerAppData
59
61
  | { type: string; [k: string]: unknown }; // forward-compat: unknown types ignored
60
62
 
61
63
  // Track first-seen components per `chatId:nodeId` so a repeated COMPONENT_INIT
@@ -74,12 +76,12 @@ export function applyServerMessage(store: ComponentStore, msg: ServerMessage, se
74
76
  const m = msg as ServerComponentInit;
75
77
  if (!m.chatId || !m.nodeId) return;
76
78
  // TEMPLATE DIRECTIVE — a template is a SERVED MCP app (protocol §0.1), loaded via
77
- // resources/read unoverse://templates/{name}; it is NEVER a streamed component. Record
79
+ // resources/read unoverse://apps/{org}/{name}/definition; it is NEVER a streamed component. Record
78
80
  // WHICH app to load (the channel reads it); do not place it in the component timeline
79
81
  // (that produced "Unknown Unoverse component: <Template>"). The legacy componentUrl
80
82
  // .js bundle is ignored — Unoverse loads the neutral definition, not a React bundle.
81
- if (m.metadata?.isTemplate === true || m.nodeId.endsWith("_template")) {
82
- if (m.component?.type) store.setActiveTemplate(m.component.type, m.component.props ?? {});
83
+ if (m.metadata?.isApp === true || m.nodeId.endsWith("_template")) {
84
+ if (m.component?.type) store.setActiveApp(m.component.type, m.component.props ?? {});
83
85
  return;
84
86
  }
85
87
  const key = `${m.chatId}:${m.nodeId}`;
@@ -105,9 +107,9 @@ export function applyServerMessage(store: ComponentStore, msg: ServerMessage, se
105
107
  case "WORKFLOW_STATE": {
106
108
  const m = msg as ServerWorkflowState;
107
109
  // OFFICIAL template selection (SPEC §5b): the workflow names the app shell in
108
- // metadata.template; the channel loads it via resources/read. (templateMode
110
+ // metadata.app; the channel loads it via resources/read. (appMode
109
111
  // switch/stack/replace is future — default "switch".)
110
- if (m.metadata?.template) store.setActiveTemplate(m.metadata.template);
112
+ if (m.metadata?.app) store.setActiveApp(m.metadata.app);
111
113
  // Open an EMPTY streaming turn so the timeline shows a thinking state BEFORE the
112
114
  // first component (legacy processWorkflowState.ts; aiContext treats these as
113
115
  // streaming). COMPONENT_INIT then reuses it. Without this, the assistant turn is
@@ -137,12 +139,12 @@ export function applyServerMessage(store: ComponentStore, msg: ServerMessage, se
137
139
  }
138
140
  // No AUDIO_STATE case: voice is a NATIVE SERVICE on the WS audio lane (useVoiceService),
139
141
  // NOT the structured MCP stream. Audio-state events never arrive here (UNOVERSE_SPEC §2e-1).
140
- case "TEMPLATE_DATA": {
142
+ case "APP_DATA": {
141
143
  // Generic write into TEMPLATE STATE (the template-state twin of COMPONENT_DATA). The
142
144
  // PRODUCER names the keys (the Suggestions node sends `{ faqs }`); the template picks
143
145
  // them up. Core knows NO key names — fully generic (UNOVERSE_STATE_MODEL §2/§8).
144
146
  const data = ((msg as { data?: Record<string, unknown> }).data ?? {}) as Record<string, unknown>;
145
- store.mergeTemplateState(data);
147
+ store.mergeAppState(data);
146
148
  return;
147
149
  }
148
150
  default:
@@ -22,7 +22,7 @@ export {
22
22
  type ServerComponentData,
23
23
  type ServerWorkflowState,
24
24
  type ServerSessionReady,
25
- type ServerTemplateData,
25
+ type ServerAppData,
26
26
  } from "./connection";
27
27
 
28
28
  // The action interpreter (§2e-3) — local `setValue` store write + server route.
@@ -45,7 +45,7 @@ export {
45
45
  resolveLatch,
46
46
  selectPointers,
47
47
  viewOf,
48
- } from "./templates";
48
+ } from "./apps";
49
49
 
50
50
  // Experience analytics (UNOVERSE_ANALYTICS.md) — the SDK emits; the host decides where to.
51
51
  export {
@@ -11,10 +11,10 @@
11
11
  * turn holds POINTERS (the `chatId:nodeId` keys) into the data
12
12
  * plane — never copies. This is what templates read.
13
13
  *
14
- * Plus a generic TEMPLATE-STATE bag (`templateState`) + `lifecycle` (derived from the
14
+ * Plus a generic TEMPLATE-STATE bag (`appState`) + `lifecycle` (derived from the
15
15
  * conversation) + `draft` (the shared composer buffer). The SDK hardcodes NO UI concept —
16
16
  * disclosure/focus/etc. are keys the dev writes via the `setValue` (component) or
17
- * `setTemplateValue` (template) actions and reads via `visibleWhen`. (Voice is a native
17
+ * `setAppValue` (template) actions and reads via `visibleWhen`. (Voice is a native
18
18
  * SERVICE — WS audio lane, react package — whose call state a producer merges into
19
19
  * template state, per UNOVERSE_SPEC §2e-1. The store holds structured state only.)
20
20
  *
@@ -92,12 +92,12 @@ export class ComponentStore {
92
92
  private notifyScheduled = false;
93
93
  private seq = 0;
94
94
  // --- active template (the workflow-selected MCP app shell; §0.1) ---
95
- private activeTemplateName: string | null = null;
96
- private activeTemplateProps: Record<string, unknown> = {};
95
+ private activeAppName: string | null = null;
96
+ private activeAppProps: Record<string, unknown> = {};
97
97
  // --- template state: the active template's OWN generic bag (UNOVERSE_STATE_MODEL §2).
98
98
  // draft / suggestions / openPanel / focusedId / voice call state all live here as
99
99
  // plain keys — core knows NO key names. Replace/merge only (O(1), never append). ---
100
- private templateState: Record<string, unknown> = {};
100
+ private appState: Record<string, unknown> = {};
101
101
  // --- history cap (UNOVERSE_STATE_MODEL §6a): hold at most N turns, oldest-out-first. ---
102
102
  private maxTurns: number;
103
103
 
@@ -130,35 +130,35 @@ export class ComponentStore {
130
130
 
131
131
  /** The current composer draft (a key in template state). */
132
132
  getDraft(): string {
133
- return (this.templateState.draft as string) ?? "";
133
+ return (this.appState.draft as string) ?? "";
134
134
  }
135
135
  /** Set the composer draft (input onChange writes here; a send button reads it). */
136
136
  setDraft(text: string): void {
137
137
  if (this.getDraft() === text) return;
138
- this.mergeTemplateState({ draft: text });
138
+ this.mergeAppState({ draft: text });
139
139
  }
140
140
 
141
141
  /** The workflow-selected template (MCP app shell) + its props, or null if the channel picks. */
142
- getActiveTemplate(): { name: string | null; props: Record<string, unknown> } {
143
- return { name: this.activeTemplateName, props: this.activeTemplateProps };
142
+ getActiveApp(): { name: string | null; props: Record<string, unknown> } {
143
+ return { name: this.activeAppName, props: this.activeAppProps };
144
144
  }
145
145
  /**
146
146
  * Switch the active template (a workflow "render this app" directive — a COMPONENT_INIT
147
- * marked isTemplate). Templates are SERVED apps, never streamed components (protocol
147
+ * marked isApp). Templates are SERVED apps, never streamed components (protocol
148
148
  * §0.1); this records WHICH app the channel should load, never a timeline component.
149
149
  */
150
- setActiveTemplate(name: string, props: Record<string, unknown> = {}): void {
151
- if (this.activeTemplateName === name && JSON.stringify(this.activeTemplateProps) === JSON.stringify(props)) return;
150
+ setActiveApp(name: string, props: Record<string, unknown> = {}): void {
151
+ if (this.activeAppName === name && JSON.stringify(this.activeAppProps) === JSON.stringify(props)) return;
152
152
  // TEMPLATE SWAP = the hard refresh boundary: a NEW shell starts with every surface
153
153
  // retired — every instance (durable ones included) returns to inline; the new
154
154
  // template derives its base state. Conversation data stays untouched (turns +
155
155
  // slices persist; only the active VIEW resets) — swapping back re-presents nothing
156
156
  // until something surfaces again.
157
- if (this.activeTemplateName !== null && this.activeTemplateName !== name) {
157
+ if (this.activeAppName !== null && this.activeAppName !== name) {
158
158
  for (const [key, slice] of this.data) this.retractSlice(key, slice);
159
159
  }
160
- this.activeTemplateName = name;
161
- this.activeTemplateProps = props;
160
+ this.activeAppName = name;
161
+ this.activeAppProps = props;
162
162
  this.bump();
163
163
  }
164
164
 
@@ -235,12 +235,12 @@ export class ComponentStore {
235
235
  }
236
236
 
237
237
  /** The active template's state bag (templates read this into their root scope). */
238
- getTemplateState(): Readonly<Record<string, unknown>> {
239
- return this.templateState;
238
+ getAppState(): Readonly<Record<string, unknown>> {
239
+ return this.appState;
240
240
  }
241
241
  /** Merge a patch into template state (replace/merge — never append, stays O(1)). */
242
- mergeTemplateState(patch: Record<string, unknown>): void {
243
- this.templateState = { ...this.templateState, ...patch };
242
+ mergeAppState(patch: Record<string, unknown>): void {
243
+ this.appState = { ...this.appState, ...patch };
244
244
  this.bump();
245
245
  }
246
246
 
@@ -249,15 +249,15 @@ export class ComponentStore {
249
249
  // NO UI concept (no panel / focus / faq / voice). The dev picks the key and the home:
250
250
  // • COMPONENT state — a component's own data + view state (tab / edit / wizard-step /
251
251
  // displayState / …), keyed by its id, written via the `setValue` action (data plane).
252
- // • TEMPLATE state — the active template's own bag (`templateState`), written via the
253
- // `setTemplateValue` action (or `mergeTemplateState` from a producer). Just keys.
252
+ // • TEMPLATE state — the active template's own bag (`appState`), written via the
253
+ // `setAppValue` action (or `mergeAppState` from a producer). Just keys.
254
254
  // `lifecycle` is the one thing that is NEITHER: it's READ OFF the conversation (the turn's
255
255
  // streaming/complete status), so it stays a tiny derived machine here. `draft` is the one
256
256
  // named convenience (the shared composer buffer the channel reads to send).
257
257
  // (Voice is a NATIVE SERVICE — WS audio lane, react package — whose call state a producer
258
258
  // merges into template state. Not a store machine.)
259
259
  // ⛔ Do NOT add a UI-pattern method here (togglePanel/openFocus/…). It's a key the dev
260
- // writes via setValue / setTemplateValue and reads via `visibleWhen`. Removed twice.
260
+ // writes via setValue / setAppValue and reads via `visibleWhen`. Removed twice.
261
261
  // ════════════════════════════════════════════════════════════════════════════
262
262
 
263
263
  // --- lifecycle: the workflow turn's status (derived from WORKFLOW_STATE) ---
@@ -171,8 +171,8 @@ export interface ActionValue {
171
171
  * (optionally narrowed by `send`). `then` chains a follow-up (Stac Multi Action). */
172
172
  export interface ActionSpec {
173
173
  // The SDK hardcodes no UI concept — only generic writes + a server route:
174
- type: string; // "setValue" (component slice) | "setTemplateValue" (template state) | "input" | "submit"/custom (→ server)
175
- values?: ActionValue[]; // setValue / setTemplateValue patches (the dev's chosen keys)
174
+ type: string; // "setValue" (component slice) | "setAppValue" (template state) | "input" | "submit"/custom (→ server)
175
+ values?: ActionValue[]; // setValue / setAppValue patches (the dev's chosen keys)
176
176
  send?: string[]; // server route: which scope fields to include (default: all)
177
177
  then?: ActionSpec | string; // optional chained follow-up
178
178
  /** Carried here by the renderer, NOT authored on the action: a node declares
@@ -2,8 +2,8 @@ export { UnoverseComponent } from "./UnoverseComponent";
2
2
  export type { UnoverseComponentProps } from "./UnoverseComponent";
3
3
  export { StreamedUnoverseComponent, useUnoverseInstance } from "./streamed";
4
4
  export type { StreamedUnoverseComponentProps } from "./streamed";
5
- export { StreamedUnoverseTemplate, selectPointers, computeAppWidth, resolveActiveLayout } from "./template";
6
- export type { StreamedUnoverseTemplateProps } from "./template";
5
+ export { StreamedUnoverseApp, selectPointers, computeAppWidth, resolveActiveLayout } from "./app";
6
+ export type { StreamedUnoverseAppProps } from "./app";
7
7
  export { renderNode, styleToCss, keyframesCss } from "./render";
8
8
  export type { ActionHandler, SlotResolver } from "./render";
9
9
  export { useUnoverseTheme } from "./theme";
@@ -59,7 +59,7 @@ export type {
59
59
  ServerComponentInit,
60
60
  ServerComponentData,
61
61
  ServerWorkflowState,
62
- ServerTemplateData,
62
+ ServerAppData,
63
63
  ServerSessionReady,
64
64
  AnalyticsSpec,
65
65
  AnalyticsEvent,
@@ -81,7 +81,7 @@ export function nodeAction(node: UnoverseNode, fallback: string): string | Actio
81
81
  */
82
82
  export type SlotResolver = (node: UnoverseNode, key?: React.Key) => ReactNode;
83
83
 
84
- // cssWidth moved to core/templates.ts (computeAppWidth needs it and core imports
84
+ // cssWidth moved to core/apps.ts (computeAppWidth needs it and core imports
85
85
  // nothing from the React layer); re-exported to keep this import path working.
86
86
  export { cssWidth };
87
87
 
@@ -12,7 +12,7 @@
12
12
  * voice action names to the actions — see the workbench wiring. The SDK owns NO styles
13
13
  * and exposes only NEUTRAL model facts here (never UX flags).
14
14
  *
15
- * Transport split (docs/UNOVERSE_MCP_TEMPLATE_PROTOCOL §5): the live PCM frames AND the
15
+ * Transport split (docs/UNOVERSE_MCP_APP_PROTOCOL §5): the live PCM frames AND the
16
16
  * synchronized audio-state control events both ride the /ws/gravity WS lane (they stay
17
17
  * frame-synchronized); only structured component data is on MCP, untouched by voice.
18
18
  */
@@ -318,7 +318,7 @@ export function useVoiceService(config: UseVoiceServiceConfig): VoiceService {
318
318
  // host-side props spread. Low-frequency (call phases + a 1Hz duration tick).
319
319
  const store = config.store;
320
320
  useEffect(() => {
321
- store?.mergeTemplateState(state as unknown as Record<string, unknown>);
321
+ store?.mergeAppState(state as unknown as Record<string, unknown>);
322
322
  }, [store, state]);
323
323
 
324
324
  return { state, startCall, endCall, toggleMute };