@unoverse-platform/studio 0.1.116 → 0.1.117

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/local/scaffold.d.mts +1 -0
  2. package/local/scaffold.mjs +12 -0
  3. package/package.json +1 -1
  4. package/screens/AgentChain.tsx +41 -25
  5. package/screens/AgentsView.tsx +41 -1
  6. package/screens/TemplatesLibraryView.tsx +32 -3
  7. package/screens/TemplatesView.tsx +2 -2
  8. package/screens/index.ts +1 -1
  9. package/src/host.ts +5 -0
  10. package/src/index.css +11 -0
  11. package/vendor/base/src/agent-mcp/discovery.ts +1 -1
  12. package/vendor/base/src/agent-mcp/index.ts +3 -1
  13. package/vendor/base/src/agent-mcp/invoke.ts +62 -18
  14. package/vendor/base/src/agent-mcp/progressive.ts +124 -0
  15. package/vendor/base/src/agent-mcp/tools.ts +1 -1
  16. package/vendor/base/src/definitions/load/expand/inherit.ts +1 -1
  17. package/vendor/base/src/definitions/load/expand/refs.ts +1 -1
  18. package/vendor/base/src/definitions/load/expand/walk.ts +1 -1
  19. package/vendor/base/src/definitions/registry/agentFolders.ts +2 -2
  20. package/vendor/base/src/definitions/registry/catalog.ts +1 -1
  21. package/vendor/base/src/definitions/registry/objects.ts +94 -6
  22. package/vendor/base/src/definitions/registry/pipelines.ts +13 -2
  23. package/vendor/base/src/http/uwsFetchAdapter.ts +39 -3
  24. package/vendor/base/src/items/collect.ts +10 -0
  25. package/vendor/base/src/items/publish.ts +10 -0
  26. package/vendor/base/src/lint/design/index.mjs +4 -1
  27. package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
  28. package/vendor/base/src/lint/objects/index.mjs +1 -1
  29. package/vendor/base/src/lint/objects/rules.d.mts +3 -0
  30. package/vendor/base/src/lint/objects/rules.mjs +93 -5
  31. package/vendor/base/src/manifests/compose.ts +3 -0
  32. package/vendor/base/src/manifests/executor/callback.ts +3 -2
  33. package/vendor/base/src/manifests/executor/memory.ts +27 -0
  34. package/vendor/base/src/manifests/executor/toolBridge.ts +70 -11
  35. package/vendor/base/src/manifests/runtime/auth/oauth.ts +43 -10
  36. package/vendor/base/src/manifests/runtime/events.ts +1 -1
  37. package/vendor/base/src/manifests/runtime/http/request.ts +43 -3
  38. package/vendor/base/src/manifests/runtime/loops/poll.ts +27 -5
  39. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +1 -1
  40. package/vendor/base/src/platform/inbound.ts +67 -0
  41. package/vendor/base/src/platform/tokenCost.ts +27 -8
  42. package/vendor/sdk/lib/core/actions.ts +10 -0
  43. package/vendor/sdk/lib/core/apps.ts +2 -2
  44. package/vendor/sdk/lib/core/client.ts +11 -0
  45. package/vendor/sdk/lib/core/store.ts +1 -1
  46. package/vendor/sdk/lib/core/types.ts +2 -0
  47. package/vendor/sdk/lib/dom/app.ts +3 -2
  48. package/vendor/sdk/lib/dom/component.ts +38 -1
  49. package/vendor/sdk/lib/dom/element.ts +23 -0
  50. package/vendor/sdk/lib/dom/primitives.ts +97 -6
  51. package/vendor/sdk/lib/voiceService.ts +3 -1
  52. package/vendor/sdk/main.ts +1 -1
@@ -29,7 +29,7 @@ import { guestHeldPointers } from "../transition/guestHold";
29
29
  import { keyframesCss, styleToCss, type CssStyle } from "../style";
30
30
  import type { ActionHandler } from "../actions";
31
31
  import { mountComponent, type ComponentMount, type DefinitionSource } from "./component";
32
- import { setChildren, setStyle } from "./element";
32
+ import { printWhenStill, setChildren, setStyle } from "./element";
33
33
  import { isolatedRoot } from "./isolate";
34
34
  import { mountQueue } from "./mounted";
35
35
  import { regionView, type Leaf, type Region, type SlotCompute } from "./region";
@@ -273,6 +273,7 @@ export function mountApp(options: AppOptions): AppMount {
273
273
  chatId: "",
274
274
  nodeId: "",
275
275
  sendToServer: o.onAction ? (t, payload) => o.onAction!(t, payload, meta) : undefined,
276
+ print: printWhenStill,
276
277
  });
277
278
  };
278
279
 
@@ -298,7 +299,7 @@ export function mountApp(options: AppOptions): AppMount {
298
299
 
299
300
  const leaf = (pointer: string, extraData?: Data, fill?: boolean) => {
300
301
  const { chatId, nodeId } = store.split(pointer);
301
- const opts = { client: o.client, store, chatId, nodeId, onAction: dispatch, theme, extraData, fill, doc };
302
+ const opts = { client: o.client, store, chatId, nodeId, onAction: dispatch, theme, extraData, fill, doc, appStore: store };
302
303
  // Keyed by the INSTANCE: one re-delivered later keeps its elements (no flicker).
303
304
  return { key: nodeId, make: (): Leaf => mountComponent(opts), refresh: (l: Leaf) => (l as ComponentMount).update(opts) };
304
305
  };
@@ -16,7 +16,7 @@ import { matches } from "../core/conditions";
16
16
  import { heldPointer, stateOfUri } from "../core/apps";
17
17
  import { keyframesCss } from "../style";
18
18
  import type { ActionHandler } from "../actions";
19
- import { setChildren, setStyle } from "./element";
19
+ import { printWhenStill, setChildren, setStyle } from "./element";
20
20
  import { isolatedRoot } from "./isolate";
21
21
  import { mountQueue } from "./mounted";
22
22
  import { regionView, type Leaf, type Region } from "./region";
@@ -26,6 +26,8 @@ import type { Data } from "./types";
26
26
  /** What reads definitions and themes: the MCP client, or anything answering the same calls. */
27
27
  export interface DefinitionSource extends ThemeSource {
28
28
  readDefinition(address: string): Promise<UnoverseDefinition>;
29
+ /** Runs the task a component carries (its lifecycle hook) as the viewer; absent, no hook runs here. */
30
+ runTask?(uri: string, phase: "onStart" | "onEnterView", props?: Record<string, unknown>, state?: string): Promise<Record<string, unknown>>;
29
31
  }
30
32
 
31
33
  export interface ComponentOptions {
@@ -129,6 +131,7 @@ export function mountComponent(options: ComponentOptions): ComponentMount {
129
131
  appStore,
130
132
  // Server actions carry the INSTANCE context (which component, its current slice).
131
133
  sendToServer: onAction ? (t, payload) => onAction(t, payload, { chatId, nodeId, type: store.getType(chatId, nodeId), state: store.get(chatId, nodeId) }) : undefined,
134
+ print: printWhenStill,
132
135
  });
133
136
  return;
134
137
  }
@@ -138,6 +141,10 @@ export function mountComponent(options: ComponentOptions): ComponentMount {
138
141
  if (values.length) {
139
142
  localSlice = { ...localSlice, ...Object.fromEntries(values.map((v) => [v.key, v.value])) };
140
143
  draw();
144
+ // A CHANGED VIEW IS NEWS here too: a live card's stream reports it and the server runs
145
+ // the `onEnterView` hook; a card drawn from a record asks for the same hook itself.
146
+ const face = values.find((v) => v.key === "state" || v.key === "view" || v.key === "defaultState");
147
+ if (face && typeof face.value === "string" && face.value) enterView(face.value);
141
148
  }
142
149
  }
143
150
  o.onAction?.(action, scope, meta);
@@ -190,6 +197,7 @@ export function mountComponent(options: ComponentOptions): ComponentMount {
190
197
  def = d;
191
198
  defaults = Object.fromEntries(Object.entries((d.props ?? {}) as Record<string, { default?: unknown }>).map(([k, v]) => [k, v?.default]));
192
199
  draw();
200
+ runHooks(asked);
193
201
  })
194
202
  .catch((e) => {
195
203
  if (!alive || defUri !== asked) return;
@@ -198,6 +206,35 @@ export function mountComponent(options: ComponentOptions): ComponentMount {
198
206
  });
199
207
  };
200
208
 
209
+ /**
210
+ * A COMPONENT FETCHES ITS OWN DATA WHEREVER IT IS DRAWN (docs/design/lifecycle-hooks.md). A live
211
+ * card's hooks run on the app's stream: `onStart` when the server renders it, `onEnterView` when
212
+ * its state changes. A card drawn from a record has no stream, so it asks the server to run the
213
+ * same hooks here, with the record's fields as `props`: `onStart`, then `onEnterView` for the
214
+ * state it opens in. What comes back merges into the drawing as it arrives, as on the stream.
215
+ * Fire-and-forget: the drawing never waits, and a failed hook leaves the card as it was.
216
+ */
217
+ const mergeTask = (uri: string) => (fields: Record<string, unknown>) => {
218
+ if (!alive || defUri !== uri || !fields || !Object.keys(fields).length) return;
219
+ localSlice = { ...localSlice, ...fields };
220
+ draw();
221
+ };
222
+ const quietTask = (uri: string) => (e: unknown) => console.warn(`[unoverse] "${uri}" task failed:`, e);
223
+ /** The `onEnterView` hook for a state this record-drawn card just entered, with its fields so far. */
224
+ const enterView = (state: string) => {
225
+ const uri = defUri;
226
+ if (o.store || !o.client.runTask || !uri) return;
227
+ const data = { ...defaults, ...(def?.state ?? {}), ...(sourceData() ?? {}), ...localSlice } as Data;
228
+ o.client.runTask(uri, "onEnterView", data, state).then(mergeTask(uri)).catch(quietTask(uri));
229
+ };
230
+ const runHooks = (uri: string) => {
231
+ if (o.store || !o.client.runTask) return;
232
+ const data = (sourceData() ?? {}) as Data;
233
+ const opening = String(data.state ?? (def?.state as Data | undefined)?.state ?? "");
234
+ o.client.runTask(uri, "onStart", data).then(mergeTask(uri)).catch(quietTask(uri));
235
+ if (opening) enterView(opening);
236
+ };
237
+
201
238
  const loadTheme = () => {
202
239
  if (o.theme || !def) return;
203
240
  const name = def.org ? `${def.org}/light` : "light";
@@ -63,6 +63,29 @@ export function setChildren(parent: Node, nodes: Node[]): void {
63
63
  while (parent.childNodes.length > i) parent.removeChild(parent.lastChild!);
64
64
  }
65
65
 
66
+ /**
67
+ * The web host's `print` action: print once the screen is still (fullscreen ended, no animation
68
+ * running, a view transition included, and the document unchanged for 300ms), so the preview never
69
+ * captures a page mid-change. Resolves when the dialog closes. Capped at five seconds.
70
+ */
71
+ export function printWhenStill(): Promise<void> {
72
+ return new Promise((done) => {
73
+ let quietSince = performance.now();
74
+ const mo = new MutationObserver(() => (quietSince = performance.now()));
75
+ mo.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
76
+ const started = performance.now();
77
+ const tick = () => {
78
+ const now = performance.now();
79
+ const animating = (document.getAnimations?.() ?? []).some((a) => a.playState === "running");
80
+ const busy = document.fullscreenElement || animating || now - quietSince < 300;
81
+ if (busy && now - started < 5000) return void requestAnimationFrame(tick);
82
+ mo.disconnect();
83
+ requestAnimationFrame(() => { window.print(); done(); });
84
+ };
85
+ requestAnimationFrame(tick);
86
+ });
87
+ }
88
+
66
89
  /** A text node whose content follows the value (strings and numbers; anything else is empty). */
67
90
  export function textOf(v: unknown): string {
68
91
  return typeof v === "string" ? v : typeof v === "number" || typeof v === "bigint" ? String(v) : "";
@@ -116,9 +116,13 @@ export function boxView(ctx: RenderCtx): Prim {
116
116
  setChildren(host, [...styles(rules), ...kids, ...(!stageSize && sel && pick && chip ? [chip] : [])]);
117
117
  };
118
118
 
119
+ // The stage scales to its frame in CSS where the browser can (container units), so a layout
120
+ // the observer never sees, print above all, still scales right; the measured scale is the fallback.
121
+ const cssScale = typeof CSS !== "undefined" && CSS.supports?.("transform", "scale(calc(100cqw / 1px))");
119
122
  const applyStage = () => {
120
123
  if (!host || !stageSize) return;
121
- setStyle(host, { ...hostStyle, position: "absolute", top: 0, left: 0, width: stageSize[0], height: stageSize[1], transform: `scale(${scale})`, transformOrigin: "0 0" });
124
+ const transform = cssScale ? `scale(calc(100cqw / ${stageSize[0]}px))` : `scale(${scale})`;
125
+ setStyle(host, { ...hostStyle, position: "absolute", top: 0, left: 0, width: stageSize[0], height: stageSize[1], transform, transformOrigin: "0 0" });
122
126
  };
123
127
 
124
128
  const selection: Behaviour = (el) => {
@@ -149,6 +153,91 @@ export function boxView(ctx: RenderCtx): Prim {
149
153
  return () => d.removeEventListener("mouseup", up);
150
154
  };
151
155
 
156
+ // `pan`: DRAG TO MOVE AROUND a box whose content is larger than it (a zoomed map, a wide table).
157
+ // Press and drag scrolls the box's own overflow, the way a map pans; the wheel, a trackpad and a
158
+ // touch drag already do. When the content grows or shrinks (zoomed in or out), the view keeps the
159
+ // same point in its centre. Opt-in per box (`pan: true`); generic, it knows no content.
160
+ const pan: Behaviour = (el) => {
161
+ let drag: { x: number; y: number; left: number; top: number; id: number } | null = null;
162
+ let moved = false;
163
+ const down = (e: PointerEvent) => {
164
+ if (e.button !== 0) return;
165
+ drag = { x: e.clientX, y: e.clientY, left: el.scrollLeft, top: el.scrollTop, id: e.pointerId };
166
+ moved = false;
167
+ };
168
+ // A DRAG SELECTS NOTHING: no text highlight and no picture picked up and dragged off, which the
169
+ // browser otherwise does to whatever the pointer passes over.
170
+ const noDrag = (e: Event) => e.preventDefault();
171
+ const move = (e: PointerEvent) => {
172
+ if (!drag || e.pointerId !== drag.id) return;
173
+ const dx = e.clientX - drag.x;
174
+ const dy = e.clientY - drag.y;
175
+ if (!moved && Math.abs(dx) + Math.abs(dy) < 4) return;
176
+ if (!moved) {
177
+ moved = true;
178
+ el.setPointerCapture?.(drag.id);
179
+ el.style.cursor = "grabbing";
180
+ el.ownerDocument.getSelection?.()?.removeAllRanges();
181
+ }
182
+ el.scrollLeft = drag.left - dx;
183
+ el.scrollTop = drag.top - dy;
184
+ };
185
+ const up = (e: PointerEvent) => {
186
+ if (!drag || e.pointerId !== drag.id) return;
187
+ if (moved) el.releasePointerCapture?.(drag.id);
188
+ drag = null;
189
+ el.style.cursor = "grab";
190
+ };
191
+ // A drag is not a tap: the click that ends a drag never reaches what was under the pointer.
192
+ const swallow = (e: MouseEvent) => {
193
+ if (!moved) return;
194
+ e.stopPropagation();
195
+ e.preventDefault();
196
+ moved = false;
197
+ };
198
+ // KEEP THE CENTRE through a change of content size.
199
+ let w = el.scrollWidth;
200
+ let h = el.scrollHeight;
201
+ const ro = new ResizeObserver(() => {
202
+ const nw = el.scrollWidth;
203
+ const nh = el.scrollHeight;
204
+ if (nw === w && nh === h) return;
205
+ const cx = (el.scrollLeft + el.clientWidth / 2) / (w || 1);
206
+ const cy = (el.scrollTop + el.clientHeight / 2) / (h || 1);
207
+ el.scrollLeft = cx * nw - el.clientWidth / 2;
208
+ el.scrollTop = cy * nh - el.clientHeight / 2;
209
+ w = nw;
210
+ h = nh;
211
+ });
212
+ for (const kid of Array.from(el.children)) ro.observe(kid);
213
+ // IT OPENS ON THE MIDDLE of its content, not the top-left corner.
214
+ el.scrollLeft = (el.scrollWidth - el.clientWidth) / 2;
215
+ el.scrollTop = (el.scrollHeight - el.clientHeight) / 2;
216
+ el.style.cursor = "grab";
217
+ el.style.userSelect = "none";
218
+ el.style.setProperty("-webkit-user-select", "none");
219
+ el.addEventListener("dragstart", noDrag);
220
+ el.addEventListener("selectstart", noDrag);
221
+ el.addEventListener("pointerdown", down);
222
+ el.addEventListener("pointermove", move);
223
+ el.addEventListener("pointerup", up);
224
+ el.addEventListener("pointercancel", up);
225
+ el.addEventListener("click", swallow, true);
226
+ return () => {
227
+ ro.disconnect();
228
+ el.style.cursor = "";
229
+ el.style.userSelect = "";
230
+ el.style.removeProperty("-webkit-user-select");
231
+ el.removeEventListener("dragstart", noDrag);
232
+ el.removeEventListener("selectstart", noDrag);
233
+ el.removeEventListener("pointerdown", down);
234
+ el.removeEventListener("pointermove", move);
235
+ el.removeEventListener("pointerup", up);
236
+ el.removeEventListener("pointercancel", up);
237
+ el.removeEventListener("click", swallow, true);
238
+ };
239
+ };
240
+
152
241
  const autoScroll: Behaviour = (el) => {
153
242
  const nearBottom = () => el.scrollHeight - el.scrollTop - el.clientHeight < 140;
154
243
  const stick = () => {
@@ -222,22 +311,24 @@ export function boxView(ctx: RenderCtx): Prim {
222
311
  setAttr(host, "class", chrome.className);
223
312
  setAttr(host, "data-unoverse-id", inspectId(n));
224
313
  if (stageEl && size) {
225
- setStyle(stageEl, { position: "relative", width: "100%", aspectRatio: `${size[0]} / ${size[1]}` });
314
+ setStyle(stageEl, { position: "relative", width: "100%", aspectRatio: `${size[0]} / ${size[1]}`, containerType: "inline-size" });
226
315
  applyStage();
227
316
  const outer = stageEl;
228
317
  stageWatch.set(`stage:${size[0]}`, outer, (el) => {
229
- const ro = new ResizeObserver(() => {
318
+ const measure = () => {
230
319
  scale = el.clientWidth / (stageSize?.[0] ?? 1);
231
320
  applyStage();
232
- });
321
+ };
322
+ const ro = new ResizeObserver(measure);
233
323
  ro.observe(el);
234
324
  return () => ro.disconnect();
235
325
  });
236
326
  } else {
237
327
  setStyle(host, hostStyle);
238
328
  // One behaviour per element, in this order (the element can carry only one).
239
- const which = n.autoScroll === true ? "autoScroll" : n.revealOnStick === true ? "revealOnStick" : sel ? "selection" : null;
240
- behaviour.set(which, host, which === "autoScroll" ? autoScroll : which === "revealOnStick" ? revealOnStick : selection);
329
+ const which =
330
+ n.autoScroll === true ? "autoScroll" : n.revealOnStick === true ? "revealOnStick" : n.pan === true ? "pan" : sel ? "selection" : null;
331
+ behaviour.set(which, host, which === "autoScroll" ? autoScroll : which === "revealOnStick" ? revealOnStick : which === "pan" ? pan : selection);
241
332
  if (!sel) pick = null;
242
333
  }
243
334
  fill();
@@ -25,6 +25,8 @@ export interface VoiceSession {
25
25
  chatId?: string;
26
26
  workflowId: string;
27
27
  targetTriggerNode: string;
28
+ /** The task the page's address names (docs/mcp/MCP_TASKS.md § 5c): carried with the call. */
29
+ task?: string;
28
30
  }
29
31
 
30
32
  export interface VoiceServiceConfig {
@@ -273,7 +275,7 @@ export function createVoiceService(config: VoiceServiceConfig): VoiceService {
273
275
  userId: session.userId,
274
276
  providerId: "gravity-voice",
275
277
  isAudio: true,
276
- metadata: { ...(start?.metadata ?? {}), targetTriggerNode, action: "START_CALL", isAction: true, workflowId, continuousStream: true },
278
+ metadata: { ...(session.task ? { task: session.task } : {}), ...(start?.metadata ?? {}), targetTriggerNode, action: "START_CALL", isAction: true, workflowId, continuousStream: true },
277
279
  },
278
280
  conversationId: session.conversationId,
279
281
  }),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ⛔ NO REACT ANYWHERE IN THIS SDK (owner, 2026-10-01; web/sdk/CLAUDE.md).
3
3
  *
4
- * The `ui://` app entry (§6d): what a HOST loads (the unoverse server's SAB host in an iframe,
4
+ * The `ui://` app entry (§6d): what a HOST loads (the unoverse server's a host in an iframe,
5
5
  * or Claude/ChatGPT). Boots the app engine on the LOCAL vendored SDK. The host provides config
6
6
  * (server, bearer, session facts) via `window.__UNOVERSE_APP_CONFIG` or a postMessage
7
7
  * `{ type: "unoverse:config", config }` (also the token-refresh channel).