@unoverse-platform/studio 0.1.35 → 0.1.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -373,6 +373,16 @@ export function TemplatesView({
373
373
  [canConnect, rawConn],
374
374
  );
375
375
 
376
+ // The workflow can OVERRIDE the manual pick (WORKFLOW_STARTED metadata.template →
377
+ // store.activeTemplate) with a BARE name — so the org must be resolved for whichever
378
+ // template is actually loading, not the picked one, or the resources/read goes out
379
+ // org-less and the server refuses it ("template address needs its org").
380
+ const activeName = conn.activeTemplate ?? selected;
381
+ const activeDef = useMemo(
382
+ () => (activeName === selected ? selectedDef : (templates.find((t) => t.name === activeName) ?? selectedDef)),
383
+ [templates, activeName, selected, selectedDef],
384
+ );
385
+
376
386
  // The `voice` native service (§2e-1) now lives INSIDE the SDK renderer (shared by every
377
387
  // host — see StreamedUnoverseTemplate). The host only DECLARES the app is voice (off its
378
388
  // manifest `service`) and hands the renderer the connection identity it already holds;
@@ -1005,6 +1015,11 @@ export function TemplatesView({
1005
1015
  conversationId={session.conversationId}
1006
1016
  onStatus={setNativeStatus}
1007
1017
  />
1018
+ ) : !activeDef && templates.length === 0 ? (
1019
+ /* The workflow can announce its template BEFORE /dev/templates answers.
1020
+ Reading it org-less is a guaranteed server refusal, so hold one beat
1021
+ until the list arrives and the org can be resolved. */
1022
+ null
1008
1023
  ) : (
1009
1024
  <StreamedUnoverseTemplate
1010
1025
  client={client}
@@ -1012,7 +1027,7 @@ export function TemplatesView({
1012
1027
  // The workflow can SELECT the template (WORKFLOW_STARTED metadata.template →
1013
1028
  // store.activeTemplate); that overrides the manual pick. Loaded via the
1014
1029
  // official resources/read unoverse://templates/{name} (templates = MCP apps).
1015
- uri={`unoverse://templates/${selectedDef?.org ? `${selectedDef.org}/` : ""}${conn.activeTemplate ?? selected}`}
1030
+ uri={`unoverse://templates/${activeDef?.org ? `${activeDef.org}/` : ""}${activeName}`}
1016
1031
  theme={theme}
1017
1032
  // Voice state arrives via TEMPLATE STATE (the service is a producer inside
1018
1033
  // the renderer); startCall/endCall/toggleMute are answered there too.
@@ -68,7 +68,15 @@ export function AppEngine(cfg: AppConfig) {
68
68
  const [def, setDef] = useState<UnoverseDefinition | null>(null);
69
69
  const lastWidthRef = useRef<string | undefined>(undefined);
70
70
  // Named app sizes resolve via the served theme (theme.appSize), like every token.
71
- const theme = useUnoverseTheme(client, def ? (def.org ? `${def.org}/light` : "light") : null);
71
+ // The theme's name is the template's org. When templateId already carries it
72
+ // ("org/name" — every production boot), start the theme read IMMEDIATELY, in
73
+ // parallel with the template read; waiting for the definition just to learn an org
74
+ // we were handed serialized two round trips for nothing. Bare ids (dev) still wait.
75
+ const orgFromId = templateId.includes("/") ? templateId.split("/")[0] : null;
76
+ const theme = useUnoverseTheme(
77
+ client,
78
+ orgFromId ? `${orgFromId}/light` : def ? (def.org ? `${def.org}/light` : "light") : null,
79
+ );
72
80
 
73
81
  // The app owns which workflow it runs — read it from its manifest (§4b), not from config.
74
82
  useEffect(() => {
@@ -119,6 +127,17 @@ export function AppEngine(cfg: AppConfig) {
119
127
  }, [surface, conversationId, userId, analyticsDebug]);
120
128
 
121
129
  useEffect(() => {
130
+ // A NEW APP RE-ANNOUNCES ITS WIDTH, ALWAYS. The native host keeps ONE iframe for every
131
+ // app (its srcdoc is keyed on the server, not the app — it just re-posts
132
+ // `unoverse:config` with a new templateId) and this engine is re-rendered in place, so
133
+ // the change-guard below outlives the switch — while the host RESETS its frame to the
134
+ // default on it (NativeAppHost/TemplatesView). Two apps of the same declared width is
135
+ // the ordinary case (one org, one `chat` size), and the incoming app then reported
136
+ // nothing: its frame sat at the host default (100%) while its core panel held its
137
+ // declared width, leaving a dead band beside it (observed live 2026-08-13,
138
+ // bpp-voice-layout). Clearing the guard here makes the first report after an app
139
+ // switch unconditional; the guard still suppresses repeats within one app.
140
+ lastWidthRef.current = undefined;
122
141
  const report = () => {
123
142
  // NAME-SYNC: width is computed off the ACTIVE layout (multi-layout templates
124
143
  // swap whole arrangements); single-layout defs resolve to their root.
@@ -154,8 +154,10 @@ export class UnoverseClient {
154
154
  })();
155
155
  this.inflight.set(uri, read);
156
156
  // Cleared either way: a failed read must not be remembered as pending forever, or the
157
- // next asker waits on a promise that will never resolve again.
158
- read.finally(() => this.inflight.delete(uri));
157
+ // next asker waits on a promise that will never resolve again. The catch is REQUIRED:
158
+ // a bare `read.finally()` forks a second promise chain that nobody awaits, so every
159
+ // failed read also surfaced as an Uncaught (in promise) even when the caller handled it.
160
+ read.catch(() => {}).finally(() => this.inflight.delete(uri));
159
161
  return read;
160
162
  }
161
163
 
@@ -177,7 +179,7 @@ export class UnoverseClient {
177
179
  return theme;
178
180
  })();
179
181
  this.themeInflight.set(name, read);
180
- read.finally(() => this.themeInflight.delete(name));
182
+ read.catch(() => {}).finally(() => this.themeInflight.delete(name));
181
183
  return read;
182
184
  }
183
185