@unoverse-platform/studio 0.1.87 → 0.1.88

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.87",
3
+ "version": "0.1.88",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -331,11 +331,11 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
331
331
  */
332
332
  const tstate = store.getAppState() as Record<string, unknown>;
333
333
  const dismissedRaw = tstate.latchDismissed;
334
- const held = def ? resolveLatch(def, store, typeof dismissedRaw === "string" ? dismissedRaw : undefined) : null;
334
+ const held = def ? resolveLatch(def, store, ladder.view, typeof dismissedRaw === "string" ? dismissedRaw : undefined) : null;
335
335
  useEffect(() => {
336
336
  if (dismissedRaw === true) {
337
337
  // Resolve the bare flag against whoever holds it RIGHT NOW, then re-derive.
338
- const current = def ? resolveLatch(def, store) : null;
338
+ const current = def ? resolveLatch(def, store, ladder.view) : null;
339
339
  store.mergeAppState({ latchDismissed: current ? current.key : "" });
340
340
  return;
341
341
  }
@@ -353,6 +353,7 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
353
353
  held?.key,
354
354
  held?.latch,
355
355
  dismissedRaw,
356
+ ladder.view,
356
357
  def,
357
358
  store,
358
359
  tstate.latched,
@@ -279,11 +279,17 @@ export function resolveActiveView(
279
279
  * holder or null, so two latches are not a bug to prevent but a state that cannot be
280
280
  * represented.
281
281
  *
282
- * SAME WALK AS `resolveActiveView`, deliberately: the ladder is the ladder. A latchable
283
- * slice must be sitting in one of the template's declared states to hold the latch, and
284
- * the highest-ranked one wins, ties going to the most recent write. So a page retracted to
285
- * `inline` (superseded by an arrival, released by `cancelBelow`, closed by the guest)
286
- * silently stops holding it rather than leaving a pill pointing at something off-surface.
282
+ * THE HOLDER MUST BE THE STATE ON SCREEN, which is why `active` is a required argument
283
+ * rather than something derived again here. Being ON the ladder is not enough: an app whose
284
+ * ladder is [page, perfect-day] shows `page` the moment a card arrives wearing that face,
285
+ * and the composed page — still live, still ranked, just not showing — went on holding the
286
+ * latch. The guest then reads a pill naming a page that is nowhere on screen and types into
287
+ * it (observed live 2026-08-31: three search cards took the surface and "Your Perfect Day"
288
+ * stayed up over them). A pill is a promise about what the words will reach.
289
+ *
290
+ * SAME WALK AS `resolveActiveView` otherwise: the highest-ranked wins, ties going to the
291
+ * most recent write. So a page retracted to `inline` (superseded by an arrival, released by
292
+ * `cancelBelow`, closed by the guest) silently stops holding it too.
287
293
  *
288
294
  * Returns the FULL key. `chatId` alone reaches the turn, not the component inside it: a
289
295
  * turn carrying two components would hydrate the wrong one.
@@ -291,12 +297,18 @@ export function resolveActiveView(
291
297
  export function resolveLatch(
292
298
  def: UnoverseDefinition | null | undefined,
293
299
  store: ComponentStore,
300
+ /** The state the app is ACTUALLY showing (`resolveActiveView().view`). Empty = nothing is
301
+ * surfaced, so nothing can be addressed. */
302
+ active: string,
294
303
  dismissed?: string,
295
304
  ): { key: string; chatId: string; nodeId: string; latch: { title: string; icon?: string }; view: string } | null {
296
305
  const order = def?.stateOrder?.length ? def.stateOrder : undefined;
297
306
  if (!order) return null; // no ladder, no ranking — abstain rather than guess a holder
298
307
  const latches = def?.latches;
299
308
  if (!latches || !Object.keys(latches).length) return null; // no state is addressable here
309
+ // WHAT IS ON SCREEN, first: an addressable state that is not the active one is not being
310
+ // shown, and a pill over someone else's surface points at nothing the guest can see.
311
+ if (!active || !latches[active]) return null;
300
312
  // `key` is the component key this candidate came from, carried so the return can name the
301
313
  // FULL key (line 269) rather than re-deriving it from chatId, which reaches the turn and
302
314
  // not the component inside it.
@@ -316,9 +328,11 @@ export function resolveLatch(
316
328
  if (!view) continue;
317
329
  const rank = order.indexOf(view);
318
330
  if (rank < 0) continue; // not on the surface (inline/retracted): holds nothing
319
- // THE STATE DECIDES. Same lookup for every instance, so nothing has to be told it is
320
- // addressable and nothing has to be untold — the face it currently wears is the whole
321
- // question, and a face with no `latch:` is simply not in this map.
331
+ // THE STATE DECIDES, and it must be the state SHOWING. Same lookup for every instance,
332
+ // so nothing has to be told it is addressable and nothing has to be untold — the face
333
+ // it currently wears is the whole question, and a face that is not the active one is
334
+ // not on screen to be addressed.
335
+ if (view !== active) continue;
322
336
  const latch = latches[view];
323
337
  if (!latch) continue;
324
338
  const at = store.touchOf(p);