@unoverse-platform/studio 0.1.115 → 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.
- package/local/catalog.ts +3 -0
- package/local/plugin.ts +12 -9
- package/local/scaffold.d.mts +1 -0
- package/local/scaffold.mjs +12 -0
- package/package.json +1 -1
- package/screens/AgentChain.tsx +232 -0
- package/screens/AgentsView.tsx +85 -6
- package/screens/SkillsView.tsx +21 -3
- package/screens/TemplatesLibraryView.tsx +32 -3
- package/screens/TemplatesView.tsx +2 -2
- package/screens/index.ts +4 -1
- package/src/host.ts +5 -0
- package/src/index.css +11 -0
- package/vendor/base/src/agent-mcp/discovery.ts +5 -2
- package/vendor/base/src/agent-mcp/index.ts +3 -1
- package/vendor/base/src/agent-mcp/invoke.ts +199 -25
- package/vendor/base/src/agent-mcp/progressive.ts +124 -0
- package/vendor/base/src/agent-mcp/tools.ts +18 -11
- package/vendor/base/src/agent-mcp/types.ts +8 -0
- package/vendor/base/src/auth/agentSwitch.ts +52 -0
- package/vendor/base/src/auth/agentToken.ts +142 -0
- package/vendor/base/src/auth/auth.ts +7 -0
- package/vendor/base/src/definitions/load/expand/inherit.ts +1 -1
- package/vendor/base/src/definitions/load/expand/refs.ts +1 -1
- package/vendor/base/src/definitions/load/expand/walk.ts +1 -1
- package/vendor/base/src/definitions/load/load.ts +6 -1
- package/vendor/base/src/definitions/registry/agentFolders.ts +113 -0
- package/vendor/base/src/definitions/registry/catalog.ts +1 -1
- package/vendor/base/src/definitions/registry/objects.ts +109 -5
- package/vendor/base/src/definitions/registry/pipelines.ts +24 -5
- package/vendor/base/src/definitions/registry/taskStarts.ts +76 -0
- package/vendor/base/src/definitions/registry/taskUri.ts +67 -0
- package/vendor/base/src/http/uwsFetchAdapter.ts +39 -3
- package/vendor/base/src/items/collect.ts +10 -0
- package/vendor/base/src/items/publish.ts +10 -0
- package/vendor/base/src/lint/design/index.mjs +14 -3
- package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
- package/vendor/base/src/lint/objects/index.d.mts +1 -0
- package/vendor/base/src/lint/objects/index.mjs +121 -3
- package/vendor/base/src/lint/objects/rules.d.mts +3 -0
- package/vendor/base/src/lint/objects/rules.mjs +106 -7
- package/vendor/base/src/manifests/compose.ts +3 -0
- package/vendor/base/src/manifests/executor/callback.ts +3 -2
- package/vendor/base/src/manifests/executor/memory.ts +27 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +79 -13
- package/vendor/base/src/manifests/marketplaceMcp.ts +20 -0
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +43 -10
- package/vendor/base/src/manifests/runtime/events.ts +1 -1
- package/vendor/base/src/manifests/runtime/http/request.ts +47 -4
- package/vendor/base/src/manifests/runtime/loops/poll.ts +27 -5
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +59 -24
- package/vendor/base/src/manifests/runtime/usage.ts +13 -5
- package/vendor/base/src/platform/agentBudget.ts +65 -0
- package/vendor/base/src/platform/agentCalendar.ts +87 -0
- package/vendor/base/src/platform/inbound.ts +67 -0
- package/vendor/base/src/platform/taskChanged.ts +25 -0
- package/vendor/base/src/platform/tokenCost.ts +414 -0
- package/vendor/base/src/security/permissions.ts +10 -2
- package/vendor/sdk/appEngine.ts +42 -1
- package/vendor/sdk/lib/core/actions.ts +10 -0
- package/vendor/sdk/lib/core/apps.ts +2 -2
- package/vendor/sdk/lib/core/client.ts +11 -0
- package/vendor/sdk/lib/core/store.ts +1 -1
- package/vendor/sdk/lib/core/types.ts +2 -0
- package/vendor/sdk/lib/dom/app.ts +3 -2
- package/vendor/sdk/lib/dom/component.ts +38 -1
- package/vendor/sdk/lib/dom/element.ts +23 -0
- package/vendor/sdk/lib/dom/markdown.ts +6 -2
- package/vendor/sdk/lib/dom/primitives.ts +97 -6
- package/vendor/sdk/lib/voiceService.ts +3 -1
- package/vendor/sdk/main.ts +1 -1
- package/vendor/sdk/pageInterfaces.ts +109 -8
|
@@ -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) : "";
|
|
@@ -143,10 +143,14 @@ function externalLink(env: Env): Rec {
|
|
|
143
143
|
const { props, children: kids } = t as El;
|
|
144
144
|
const theme = env.theme();
|
|
145
145
|
const { s } = prose(theme);
|
|
146
|
-
|
|
146
|
+
// A LINK TO THE SAME SITE (a path, `/signal/person/?id=…`) is the page's own next view: it
|
|
147
|
+
// opens in place, without the external glyph. Any other address opens in a new tab.
|
|
148
|
+
const href = typeof (props as { href?: unknown }).href === "string" ? String((props as { href?: unknown }).href) : "";
|
|
149
|
+
const ownSite = href.startsWith("/") && !href.startsWith("//");
|
|
150
|
+
attrs = setProps(el, ownSite ? { ...props } : { ...props, target: "_blank", rel: "noopener noreferrer" }, attrs);
|
|
147
151
|
setStyle(el, env.inheritLink() ? { ...s("link"), color: "inherit" } : s("link"));
|
|
148
152
|
const li = (theme.icons?.externalLink ?? {}) as Parameters<typeof svgOf>[1];
|
|
149
|
-
if (!li.children?.length) arrow = null;
|
|
153
|
+
if (ownSite || !li.children?.length) arrow = null;
|
|
150
154
|
else if (arrow?.glyph !== li) arrow = { glyph: li, svg: svgOf(env.doc, li, s("linkIcon")) };
|
|
151
155
|
children(env, el, recs, kids);
|
|
152
156
|
if (arrow) el.appendChild(arrow.svg);
|
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
240
|
-
|
|
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
|
}),
|
package/vendor/sdk/main.ts
CHANGED
|
@@ -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
|
|
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).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* The frame shares the page's origin (EMBED_CLIENT.md §1), so the page's document is reachable
|
|
19
19
|
* from here, and a part drawn into the page is as live as one drawn in the frame.
|
|
20
20
|
*/
|
|
21
|
+
import { taskUri, taskUriOf } from "../../../../../packages/base/src/definitions/registry/taskUri.js";
|
|
21
22
|
import { ComponentStore } from "./lib/core/store";
|
|
22
23
|
import type { UnoverseDefinition } from "./lib/core/types";
|
|
23
24
|
import type { ActionHandler, ActionMeta } from "./lib/actions";
|
|
@@ -25,6 +26,20 @@ import { mountComponent, type ComponentMount, type DefinitionSource } from "./li
|
|
|
25
26
|
import type { VoiceService } from "./lib/voiceService";
|
|
26
27
|
|
|
27
28
|
const MARK = "data-unoverse-interface";
|
|
29
|
+
/** WHAT THE PAGE HANDS A TEMPLATE'S TASK (docs/design/lifecycle-hooks.md): a JSON object on the
|
|
30
|
+
* marked element, such as the id in the page's address. Its task reads it as `props.input`;
|
|
31
|
+
* the page hands values, never calls. */
|
|
32
|
+
const INPUT = "data-unoverse-input";
|
|
33
|
+
function inputOf(el: HTMLElement): Record<string, unknown> | undefined {
|
|
34
|
+
const raw = el.getAttribute(INPUT);
|
|
35
|
+
if (!raw) return undefined;
|
|
36
|
+
try {
|
|
37
|
+
const v = JSON.parse(raw);
|
|
38
|
+
return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined;
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
const CHAT = "page";
|
|
29
44
|
const TEMPLATE_NODE = "~template";
|
|
30
45
|
|
|
@@ -60,6 +75,17 @@ function keyOwner(doc: Document): HTMLElement | null {
|
|
|
60
75
|
|
|
61
76
|
/** A page template's address, from the name its element is marked with. */
|
|
62
77
|
const uriOf = (name: string) => (name.includes("://") ? name : `unoverse://templates/${name}`);
|
|
78
|
+
/**
|
|
79
|
+
* THE TASK A PAGE TEMPLATE IS, by its one URI (docs/mcp/MCP_TASKS.md § 2a): the template's own
|
|
80
|
+
* address (`unoverse://templates/<org>/<name>`, what draws it) names the task
|
|
81
|
+
* `unoverse://tasks/<org>/<name>`, which is what starts it (`metadata.task`) and what its progress
|
|
82
|
+
* is published under (`tasks.<uri>`). The shared parser, bundled, so the page and the platform
|
|
83
|
+
* read a task's name one way.
|
|
84
|
+
*/
|
|
85
|
+
const taskOf = (uri: string): string => {
|
|
86
|
+
const short = uri.replace(/^unoverse:\/\/templates\//, "");
|
|
87
|
+
return taskUri(short) ?? taskUriOf(null, short) ?? uri;
|
|
88
|
+
};
|
|
63
89
|
|
|
64
90
|
/** THE AVATARS: page templates whose steps are the call's phases (`listening`, `speaking`).
|
|
65
91
|
* They are how the page is spoken to, so they are never the task in view; they follow the call. */
|
|
@@ -67,7 +93,7 @@ const avatars = new Set<HTMLElement>();
|
|
|
67
93
|
const CALL_PHASES = ["listening", "speaking"];
|
|
68
94
|
|
|
69
95
|
/** The workflow a template names in its manifest (UNOVERSE_HOST_PAGE.md rule 4). */
|
|
70
|
-
export type Binding = { workflow?: string; trigger?: string; pipeline?: string };
|
|
96
|
+
export type Binding = { workflow?: string; trigger?: string; pipeline?: string; autoTrigger?: boolean };
|
|
71
97
|
|
|
72
98
|
/** THE TASK IN VIEW: the marked template, not an avatar, spanning the middle of the screen. */
|
|
73
99
|
function taskInView(doc: Document): HTMLElement | null {
|
|
@@ -121,6 +147,8 @@ interface PageTemplateOptions {
|
|
|
121
147
|
waiting: () => boolean;
|
|
122
148
|
onBinding: (uri: string, binding: Binding) => void;
|
|
123
149
|
onCall: ActionHandler;
|
|
150
|
+
/** Run the task this template carries (its lifecycle hook) as the viewer; its fields come back. */
|
|
151
|
+
runTask: (uri: string, phase: "onStart", input?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
124
152
|
}
|
|
125
153
|
|
|
126
154
|
/** One marked template: read it, give it its parts, draw it. */
|
|
@@ -132,6 +160,12 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
132
160
|
let def: { stateTree?: StateTree } | null = null;
|
|
133
161
|
let comp: ComponentMount | null = null;
|
|
134
162
|
let alive = true;
|
|
163
|
+
// THE TASK IT CARRIES, ASKED AT ONCE (docs/design/lifecycle-hooks.md): in parallel with reading
|
|
164
|
+
// the template, not after it. A template without an `onStart` hook answers `{}` and is ignored.
|
|
165
|
+
const early = o.runTask(uri, "onStart", inputOf(el)).catch((e) => {
|
|
166
|
+
console.warn(`[unoverse] page template "${uri}" task failed:`, e);
|
|
167
|
+
return {} as Record<string, unknown>;
|
|
168
|
+
});
|
|
135
169
|
|
|
136
170
|
// Where the template draws: its element, or the dock while a fullscreen template holds one
|
|
137
171
|
// and this template is an avatar.
|
|
@@ -142,10 +176,14 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
142
176
|
};
|
|
143
177
|
|
|
144
178
|
const ready = () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
// DRAWN WHEN ITS DATA IS IN: a template with a task shows nothing until the task answers, so
|
|
180
|
+
// its sample values (the preview a studio draws) never flash on a live page.
|
|
181
|
+
const show = () => {
|
|
182
|
+
comp = mountComponent({ client, store, chatId: CHAT, nodeId: TEMPLATE_NODE, onAction: o.onCall, place: () => [dock.slot], appStore, doc: el.ownerDocument, arrowKeys: false });
|
|
183
|
+
place();
|
|
184
|
+
stops.push(dock.watch(place), store.subscribe(() => dock.check()));
|
|
185
|
+
dock.check();
|
|
186
|
+
};
|
|
149
187
|
|
|
150
188
|
// A TEMPLATE FOLLOWS ITS TASK. Whatever runs the task publishes where it is as
|
|
151
189
|
// `tasks.<its URI>` in the app state (SIGNAL.md 6c): this template moves to that step, and its
|
|
@@ -161,6 +199,47 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
161
199
|
for (const nodeId of parts.filter((id) => id.startsWith(`${part}~`))) store.mergeComponentState(CHAT, nodeId, values as Record<string, unknown>);
|
|
162
200
|
}
|
|
163
201
|
};
|
|
202
|
+
// A TASK'S VALUES THAT NAME NO PART are the template's own props (a list it holds, a line
|
|
203
|
+
// above it): they fill the template itself, as a part's fill its part.
|
|
204
|
+
const fillOwn = (byPart: Record<string, unknown>) => {
|
|
205
|
+
const own = Object.fromEntries(Object.entries(byPart).filter(([key]) => key !== "state" && !parts.some((id) => id.startsWith(`${key}~`))));
|
|
206
|
+
if (!Object.keys(own).length) return;
|
|
207
|
+
syncHeld({ ...((store.get(CHAT, TEMPLATE_NODE) ?? {}) as Record<string, unknown>), ...own });
|
|
208
|
+
store.mergeComponentState(CHAT, TEMPLATE_NODE, own);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// WHAT THE TEMPLATE HOLDS (docs/design/templates.md "Holding a component"): one instance of
|
|
212
|
+
// the held component per item of the template's own data, keyed as the server keys it in an
|
|
213
|
+
// app (server runtime/screen/held.ts) and as `heldPointer` looks it up, so each card draws in
|
|
214
|
+
// its spot and keeps its own state. Re-sent only when an item changes. MADE BEFORE the data
|
|
215
|
+
// that names them reaches the template: a spot looks for its instance as it draws, once.
|
|
216
|
+
const heldSpecs = Object.values(((def as { held?: Record<string, { component: string; items: string[] }[]> } | null)?.held) ?? {}).flat();
|
|
217
|
+
const lastHeld = new Map<string, string>();
|
|
218
|
+
const syncHeld = (data: Record<string, unknown>) => {
|
|
219
|
+
if (!heldSpecs.length) return;
|
|
220
|
+
for (const spec of heldSpecs) {
|
|
221
|
+
if (!(spec.items[0] in data)) continue;
|
|
222
|
+
let level: unknown[] = [data];
|
|
223
|
+
for (const field of spec.items)
|
|
224
|
+
level = level.flatMap((o) => {
|
|
225
|
+
const list = o && typeof o === "object" ? (o as Record<string, unknown>)[field] : undefined;
|
|
226
|
+
return Array.isArray(list) ? list : [];
|
|
227
|
+
});
|
|
228
|
+
level.forEach((item, index) => {
|
|
229
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return;
|
|
230
|
+
const row = item as Record<string, unknown>;
|
|
231
|
+
const id = row.universal_id ?? row.ref ?? row.id;
|
|
232
|
+
const key = typeof id === "string" || typeof id === "number" ? String(id) : String(index);
|
|
233
|
+
const nodeId = `${TEMPLATE_NODE}:${spec.component}:${key}`;
|
|
234
|
+
const next = JSON.stringify(row);
|
|
235
|
+
if (lastHeld.get(nodeId) === next) return;
|
|
236
|
+
lastHeld.set(nodeId, next);
|
|
237
|
+
if (!store.getType(CHAT, nodeId)) store.apply({ type: "COMPONENT_INIT", chatId: CHAT, nodeId, component: { type: spec.component, props: row } });
|
|
238
|
+
else store.apply({ type: "COMPONENT_DATA", chatId: CHAT, nodeId, data: row });
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
syncHeld((store.get(CHAT, TEMPLATE_NODE) ?? {}) as Record<string, unknown>);
|
|
164
243
|
const moveTo = (at: unknown) => {
|
|
165
244
|
if (typeof at !== "string") return;
|
|
166
245
|
const tree = def?.stateTree?.find((t) => t.on && t.states?.some((x) => x.name === at));
|
|
@@ -169,12 +248,13 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
169
248
|
if (tpl?.[tree.on] !== at) store.mergeComponentState(CHAT, TEMPLATE_NODE, { [tree.on]: at });
|
|
170
249
|
};
|
|
171
250
|
const follow = () => {
|
|
172
|
-
const task = ((appStore.getAppState().tasks ?? {}) as Record<string, { step?: string; props?: Record<string, unknown> }>)[uri];
|
|
251
|
+
const task = ((appStore.getAppState().tasks ?? {}) as Record<string, { step?: string; props?: Record<string, unknown> }>)[taskOf(uri)];
|
|
173
252
|
if (task?.props && typeof task.props === "object") {
|
|
174
253
|
const next = JSON.stringify(task.props);
|
|
175
254
|
if (next !== lastProps) {
|
|
176
255
|
lastProps = next;
|
|
177
256
|
fillParts(task.props);
|
|
257
|
+
fillOwn(task.props);
|
|
178
258
|
}
|
|
179
259
|
}
|
|
180
260
|
// A NEW STEP MOVES IT, once. A step the person took since (Enter) stands until the task
|
|
@@ -214,6 +294,18 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
214
294
|
follow();
|
|
215
295
|
stops.push(appStore.subscribe(follow));
|
|
216
296
|
|
|
297
|
+
// THE TASK IT CARRIES (docs/design/lifecycle-hooks.md): a template whose manifest has an
|
|
298
|
+
// `onStart` hook asks the server to run it as it loads, as the viewer, and fills its own
|
|
299
|
+
// fields with the answer, its held cards made first, as a workflow's fill does.
|
|
300
|
+
const hooks = (def as { lifecycle?: Array<{ phase?: string }> } | null)?.lifecycle ?? [];
|
|
301
|
+
if (hooks.some((h) => h.phase === "onStart"))
|
|
302
|
+
void early.then((fields) => {
|
|
303
|
+
if (!alive) return;
|
|
304
|
+
if (fields && Object.keys(fields).length) fillOwn(fields);
|
|
305
|
+
show();
|
|
306
|
+
});
|
|
307
|
+
else show();
|
|
308
|
+
|
|
217
309
|
const doc = pageDocument();
|
|
218
310
|
if (doc) {
|
|
219
311
|
// THE ARROW KEYS STEP IT, on the PAGE (the keys are the page's), when its state says
|
|
@@ -336,6 +428,7 @@ export interface PageInterfacesOptions {
|
|
|
336
428
|
appStore: ComponentStore;
|
|
337
429
|
toServer: (t: string, data: Record<string, unknown>, meta?: ActionMeta) => void;
|
|
338
430
|
startTask: (binding: Binding, task: string, submit?: Record<string, unknown>) => void;
|
|
431
|
+
runTask: (uri: string, phase: "onStart", input?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
339
432
|
}
|
|
340
433
|
|
|
341
434
|
/**
|
|
@@ -374,7 +467,7 @@ export function mountPageInterfaces(o: PageInterfacesOptions): { destroy(): void
|
|
|
374
467
|
if (action === "submit" || action === "cancel") {
|
|
375
468
|
o.toServer(action, data, meta);
|
|
376
469
|
const state = (meta?.state ?? data) as Record<string, unknown> | undefined;
|
|
377
|
-
if (action === "submit" && (bindings[uri]?.workflow || bindings[uri]?.pipeline) && state && typeof state === "object") o.startTask(bindings[uri], uri, state);
|
|
470
|
+
if (action === "submit" && (bindings[uri]?.workflow || bindings[uri]?.pipeline) && state && typeof state === "object") o.startTask(bindings[uri], taskOf(uri), state);
|
|
378
471
|
} else if (action === "startCall") {
|
|
379
472
|
const t = target();
|
|
380
473
|
if (!t || started.has(t)) return;
|
|
@@ -382,7 +475,7 @@ export function mountPageInterfaces(o: PageInterfacesOptions): { destroy(): void
|
|
|
382
475
|
refreshAll();
|
|
383
476
|
// The press is the click the browser needs before it plays a voice the workflow may have.
|
|
384
477
|
o.voice.unlock();
|
|
385
|
-
o.startTask(bindings[t], t);
|
|
478
|
+
o.startTask(bindings[t], taskOf(t));
|
|
386
479
|
} else if (action === "endCall") void o.voice.endCall();
|
|
387
480
|
else if (action === "toggleMute") o.voice.toggleMute();
|
|
388
481
|
// Another app on the page, opened or closed by its `<org>/<app>` (its own tag, EMBED_CLIENT.md).
|
|
@@ -425,9 +518,17 @@ export function mountPageInterfaces(o: PageInterfacesOptions): { destroy(): void
|
|
|
425
518
|
onBinding: (u, b) => {
|
|
426
519
|
if (bindings[u]) return;
|
|
427
520
|
bindings[u] = b;
|
|
521
|
+
// A TASK THAT STARTS ITSELF (UNOVERSE_HOST_PAGE.md rule 4b): a binding that says
|
|
522
|
+
// `autoTrigger: true` starts its task once, as the page draws the template, the way
|
|
523
|
+
// an app's does on load. Nothing to press, and never twice on one page.
|
|
524
|
+
if (b.autoTrigger && !started.has(u)) {
|
|
525
|
+
started.add(u);
|
|
526
|
+
o.startTask(b, u);
|
|
527
|
+
}
|
|
428
528
|
refreshAll();
|
|
429
529
|
},
|
|
430
530
|
onCall: onCallFor(uri),
|
|
531
|
+
runTask: o.runTask,
|
|
431
532
|
}),
|
|
432
533
|
);
|
|
433
534
|
}
|