@unoverse-platform/studio 0.1.69 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/studio.mjs +1 -1
- package/local/catalog.ts +2 -2
- package/local/mcp.ts +3 -2
- package/local/plugin.ts +13 -11
- package/local/{sample-template → sample-app}/chat/manifest.yaml +3 -3
- package/local/sample-component/text-chat/components/suggestions.yaml +3 -3
- package/local/scaffold.mjs +11 -11
- package/package.json +2 -1
- package/screens/NativeAppHost.tsx +5 -5
- package/screens/TemplatesView.tsx +77 -77
- package/screens/chrome.ts +1 -1
- package/screens/states.ts +3 -3
- package/src/App.tsx +20 -9
- package/src/NewProject.tsx +1 -1
- package/src/host.ts +2 -2
- package/vendor/sdk/appEngine.tsx +18 -18
- package/vendor/sdk/lib/{template.tsx → app.tsx} +11 -11
- package/vendor/sdk/lib/connection.tsx +10 -10
- package/vendor/sdk/lib/core/actions.ts +3 -3
- package/vendor/sdk/lib/core/{templates.ts → apps.ts} +1 -1
- package/vendor/sdk/lib/core/connection.ts +17 -15
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +22 -22
- package/vendor/sdk/lib/core/types.ts +2 -2
- package/vendor/sdk/lib/index.ts +3 -3
- package/vendor/sdk/lib/render.tsx +1 -1
- package/vendor/sdk/lib/voice.tsx +2 -2
- package/vendor/sdk/main.tsx +7 -7
- package/vendor/skills/unoverse-create/SKILL.md +9 -9
- package/vendor/skills/unoverse-create/references/{template.md → app.md} +34 -34
- package/vendor/skills/unoverse-create/references/brand.md +8 -8
- package/vendor/skills/unoverse-create/references/component.md +20 -20
- package/vendor/skills/unoverse-create/references/node.md +2 -2
- package/vendor/skills/unoverse-prompt-component/SKILL.md +2 -2
- /package/local/{sample-template → sample-app}/chat/components/chat-region.yaml +0 -0
- /package/local/{sample-template → sample-app}/chat/components/core-focus.yaml +0 -0
- /package/local/{sample-template → sample-app}/chat/components/core.yaml +0 -0
- /package/local/{sample-template → sample-app}/chat/components/focus-overlay.yaml +0 -0
- /package/local/{sample-template → sample-app}/chat/layouts/focused.yaml +0 -0
- /package/local/{sample-template → sample-app}/chat/layouts/standard.yaml +0 -0
|
@@ -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
|
|
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
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
84
|
-
// instantiates the matching service and feeds its neutral state + actions to the
|
|
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
|
|
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
|
|
93
|
-
// the
|
|
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
|
|
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
|
|
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
|
-
//
|
|
105
|
-
// multi-layout
|
|
106
|
-
//
|
|
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
|
|
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
|
|
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
|
-
// "
|
|
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
|
-
//
|
|
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:
|
|
171
|
-
// A fresh store per mode AND per app selection: a catch-all
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
436
|
-
// store.
|
|
437
|
-
//
|
|
438
|
-
// org-less and the server refuses it ("
|
|
439
|
-
const activeName = conn.
|
|
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
|
|
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
|
|
467
|
-
// STATE on this store —
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
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 } (
|
|
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
|
|
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
|
|
649
|
-
// preview the
|
|
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
|
|
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.
|
|
688
|
+
store.mergeAppState({ [c.field]: c.eq ?? true });
|
|
689
689
|
} else if (typeof when === "string" && when !== "isEmpty" && when !== "hasMessages") {
|
|
690
|
-
store.
|
|
690
|
+
store.mergeAppState({ [when]: true });
|
|
691
691
|
}
|
|
692
|
-
// Authored
|
|
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.
|
|
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/<project>/
|
|
719
|
+
<div className="p-2 text-xs text-gray-400">No apps yet — add one under design/<project>/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
|
|
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 ?? "
|
|
769
|
-
{/*
|
|
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
|
|
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 ?? "
|
|
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
|
|
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 —
|
|
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
|
|
1056
|
-
// (
|
|
1057
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
1112
|
+
<StreamedUnoverseApp
|
|
1113
1113
|
client={client}
|
|
1114
1114
|
store={store}
|
|
1115
|
-
// The workflow can SELECT the
|
|
1116
|
-
// store.
|
|
1117
|
-
// official resources/read unoverse://
|
|
1118
|
-
uri={`unoverse://
|
|
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
|
|
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
|
|
1183
|
-
// on load —
|
|
1184
|
-
//
|
|
1185
|
-
const declared = target?.app?.defaultState ?? target?.app?.mode ?? "
|
|
1186
|
-
if (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
|
-
*
|
|
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
|
|
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;
|
|
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 (
|
|
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
|
))}
|
package/src/NewProject.tsx
CHANGED
|
@@ -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/<name>/ with components, styles and
|
|
54
|
+
Creates design/<name>/ 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
|
|
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
|
|
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
|