@unoverse-platform/studio 0.1.97 → 0.1.98
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 +2 -2
- package/local/catalog.ts +5 -1
- package/local/nodes.ts +28 -41
- package/local/plugin.ts +71 -13
- package/local/sample-identity/brand.yaml +24 -0
- package/local/sample-identity/organisation.yaml +14 -0
- package/local/sample-identity/purpose.yaml +14 -0
- package/local/sample-identity/story.yaml +18 -0
- package/local/scaffold.mjs +43 -23
- package/local/source.ts +3 -8
- package/package.json +1 -1
- package/screens/AgentsView.tsx +173 -0
- package/screens/ComponentsView.tsx +2 -2
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/IdentityView.tsx +3 -3
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +2 -0
- package/src/App.tsx +8 -3
- package/src/host.ts +1 -14
- package/vendor/base/src/agent-mcp/discovery.ts +76 -18
- package/vendor/base/src/agent-mcp/handoff.ts +11 -31
- package/vendor/base/src/agent-mcp/index.ts +5 -5
- package/vendor/base/src/agent-mcp/invoke.ts +37 -19
- package/vendor/base/src/agent-mcp/lean.ts +19 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
- package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
- package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
- package/vendor/base/src/agent-mcp/tools.ts +136 -7
- package/vendor/base/src/agent-mcp/types.ts +47 -10
- package/vendor/base/src/definitions/load/compile/component.ts +59 -4
- package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
- package/vendor/base/src/definitions/load/compile/template.ts +19 -15
- package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
- package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
- package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
- package/vendor/base/src/definitions/load/load.ts +37 -28
- package/vendor/base/src/definitions/model.ts +8 -12
- package/vendor/base/src/definitions/registry/agents.ts +111 -0
- package/vendor/base/src/definitions/registry/catalog.ts +28 -12
- package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
- package/vendor/base/src/definitions/registry/homes.ts +69 -5
- package/vendor/base/src/definitions/registry/objects.ts +413 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
- package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
- package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
- package/vendor/base/src/definitions/theme.ts +1 -2
- package/vendor/base/src/items/collect.ts +29 -25
- package/vendor/base/src/items/publish.ts +30 -6
- package/vendor/base/src/lint/context.mjs +17 -0
- package/vendor/base/src/lint/design/file.mjs +199 -2
- package/vendor/base/src/lint/design/index.mjs +66 -73
- package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
- package/vendor/base/src/lint/design/walk.mjs +14 -4
- package/vendor/base/src/lint/index.mjs +15 -9
- package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
- package/vendor/base/src/lint/nodes/index.mjs +5 -0
- package/vendor/base/src/lint/nodes/node.mjs +157 -7
- package/vendor/base/src/lint/objects/index.d.mts +32 -0
- package/vendor/base/src/lint/objects/index.mjs +324 -0
- package/vendor/base/src/lint/objects/rules.d.mts +44 -0
- package/vendor/base/src/lint/objects/rules.mjs +446 -0
- package/vendor/base/src/lint/prompts/index.d.mts +34 -0
- package/vendor/base/src/lint/prompts/index.mjs +198 -0
- package/vendor/base/src/manifests/compose.ts +80 -9
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +9 -1
- package/vendor/base/src/manifests/executor/memory.ts +83 -3
- package/vendor/base/src/manifests/executor/session.ts +50 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
- package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
- package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
- package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
- package/vendor/base/src/manifests/runtime/context.ts +18 -6
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
- package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
- package/vendor/base/src/manifests/runtime/events.ts +39 -3
- package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
- package/vendor/base/src/manifests/runtime/index.ts +3 -2
- package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
- package/vendor/base/src/manifests/runtime/usage.ts +10 -0
- package/vendor/base/src/paths.ts +16 -8
- package/vendor/base/src/platform/audioManager.ts +32 -0
- package/vendor/base/src/platform/executionContext.ts +14 -0
- package/vendor/base/src/platform/liveInput.ts +52 -0
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/template/SafeExpression.ts +6 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
- package/vendor/sdk/appEngine.tsx +30 -11
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
- package/vendor/sdk/lib/app.tsx +70 -95
- package/vendor/sdk/lib/connection.tsx +47 -57
- package/vendor/sdk/lib/core/actions.ts +6 -0
- package/vendor/sdk/lib/core/apps.ts +66 -81
- package/vendor/sdk/lib/core/client.ts +5 -14
- package/vendor/sdk/lib/core/connection.ts +30 -0
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +90 -15
- package/vendor/sdk/lib/core/types.ts +9 -8
- package/vendor/sdk/lib/primitives.tsx +113 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
- package/vendor/sdk/lib/render.tsx +3 -3
- package/vendor/sdk/lib/voice.tsx +27 -13
- package/local/sample-identity/brand/brand.yaml +0 -88
- package/local/sample-identity/brand/manifest.yaml +0 -13
- package/local/sample-identity/organisation/manifest.yaml +0 -15
- package/local/sample-identity/organisation/organisation.yaml +0 -53
- package/local/sample-identity/purpose/manifest.yaml +0 -12
- package/local/sample-identity/purpose/purpose.yaml +0 -46
- package/local/sample-identity/story/manifest.yaml +0 -12
- package/local/sample-identity/story/story.yaml +0 -63
- package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Used by the intrinsic agent memory tools (PlatformMemoryTools — the Memory
|
|
7
7
|
* toggles) and the Universe Copilot harness. NOT used by spatial search —
|
|
8
|
-
* "SpatialSearch serves knowledge tools only" (
|
|
8
|
+
* "SpatialSearch serves knowledge tools only" (docs/architecture/signal/memory/MEMORY.md); the old
|
|
9
9
|
* includeMemory-in-search path is dead.
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -16,7 +16,8 @@ const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "http://localhost:4
|
|
|
16
16
|
|
|
17
17
|
export interface GetMemoryInput {
|
|
18
18
|
userId: string;
|
|
19
|
-
|
|
19
|
+
/** The organisation the run works as: the scope of a person's memory (MEMORY.md § 2.10). */
|
|
20
|
+
org: string;
|
|
20
21
|
query?: string;
|
|
21
22
|
domains?: string[];
|
|
22
23
|
minCertainty?: number;
|
|
@@ -39,10 +40,10 @@ export class GetMemoryService {
|
|
|
39
40
|
* Retrieve composed understanding for a user (uses /compose endpoint)
|
|
40
41
|
*/
|
|
41
42
|
static async compose(input: GetMemoryInput): Promise<any> {
|
|
42
|
-
const { userId,
|
|
43
|
+
const { userId, org, query, domains, accessToken } = input;
|
|
43
44
|
|
|
44
45
|
try {
|
|
45
|
-
const params = new URLSearchParams({ userId,
|
|
46
|
+
const params = new URLSearchParams({ userId, org, mode: "full" });
|
|
46
47
|
if (domains?.length) params.set("domains", domains.join(","));
|
|
47
48
|
if (query) params.set("context", query);
|
|
48
49
|
|
|
@@ -67,11 +68,11 @@ export class GetMemoryService {
|
|
|
67
68
|
/**
|
|
68
69
|
* Get recent evidence snapshot (recency-ordered, no query)
|
|
69
70
|
*/
|
|
70
|
-
static async recent(input: { userId: string;
|
|
71
|
-
const { userId,
|
|
71
|
+
static async recent(input: { userId: string; org: string; limit?: number; accessToken?: string }): Promise<MemoryEvidence[]> {
|
|
72
|
+
const { userId, org, limit, accessToken } = input;
|
|
72
73
|
|
|
73
74
|
try {
|
|
74
|
-
const params = new URLSearchParams({ userId,
|
|
75
|
+
const params = new URLSearchParams({ userId, org });
|
|
75
76
|
if (limit) params.set("limit", String(limit));
|
|
76
77
|
|
|
77
78
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
@@ -96,14 +97,14 @@ export class GetMemoryService {
|
|
|
96
97
|
* Search user evidence by query (uses /evidence/search endpoint)
|
|
97
98
|
*/
|
|
98
99
|
static async search(input: GetMemoryInput): Promise<MemoryEvidence[]> {
|
|
99
|
-
const { userId,
|
|
100
|
+
const { userId, org, query, domains, minCertainty, limit, accessToken } = input;
|
|
100
101
|
|
|
101
102
|
if (!query) {
|
|
102
103
|
return this.retrieve(input);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
try {
|
|
106
|
-
const params = new URLSearchParams({ userId,
|
|
107
|
+
const params = new URLSearchParams({ userId, org, query });
|
|
107
108
|
if (domains?.length) params.set("domains", domains.join(","));
|
|
108
109
|
if (minCertainty !== undefined) params.set("minCertainty", String(minCertainty));
|
|
109
110
|
if (limit) params.set("limit", String(limit));
|
|
@@ -130,10 +131,10 @@ export class GetMemoryService {
|
|
|
130
131
|
* Retrieve raw evidence (structured, no query needed — uses /evidence endpoint)
|
|
131
132
|
*/
|
|
132
133
|
static async retrieve(input: GetMemoryInput): Promise<MemoryEvidence[]> {
|
|
133
|
-
const { userId,
|
|
134
|
+
const { userId, org, domains, minCertainty, limit, accessToken } = input;
|
|
134
135
|
|
|
135
136
|
try {
|
|
136
|
-
const params = new URLSearchParams({ userId,
|
|
137
|
+
const params = new URLSearchParams({ userId, org });
|
|
137
138
|
if (domains?.length) params.set("domains", domains.join(","));
|
|
138
139
|
if (minCertainty !== undefined) params.set("minCertainty", String(minCertainty));
|
|
139
140
|
if (limit) params.set("limit", String(limit || 20));
|
|
@@ -38,7 +38,7 @@ const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "http://localhost:4
|
|
|
38
38
|
// Tool schemas
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
40
|
|
|
41
|
-
// The ONE user-memory tool (
|
|
41
|
+
// The ONE user-memory tool (docs/architecture/signal/memory/MEMORY.md § Serving). The standing snapshot is no
|
|
42
42
|
// longer a tool at all — the agent HARNESS injects who-the-user-is + current state
|
|
43
43
|
// into the system context at init (zero calls). queryMemory remains as the single
|
|
44
44
|
// depth door: search the user's remembered facts when a topic needs prior context.
|
|
@@ -268,6 +268,9 @@ export function mergePlatformMemorySchema(baseSchema: any | null, flags: MemoryF
|
|
|
268
268
|
interface MemoryCallIdentity {
|
|
269
269
|
userId: string;
|
|
270
270
|
workflowId: string;
|
|
271
|
+
/** The organisation the run works as: the scope of the person's memory (MEMORY.md § 2.10),
|
|
272
|
+
* and the stamp on a goal. Named by the run, else the canvas's default. */
|
|
273
|
+
org?: string;
|
|
271
274
|
agentId: string;
|
|
272
275
|
agentName?: string;
|
|
273
276
|
accessToken?: string;
|
|
@@ -279,6 +282,7 @@ function resolveIdentity(params: any, context: any): MemoryCallIdentity | { erro
|
|
|
279
282
|
const userId =
|
|
280
283
|
params?.userId || context?.workflow?.variables?.userId || context?.publishingContext?.userId;
|
|
281
284
|
const workflowId = context?.workflowId || context?.workflow?.id;
|
|
285
|
+
const org: string | undefined = context?.publishingContext?.org ?? context?.workflow?.org ?? undefined;
|
|
282
286
|
if (!userId) return { error: "No user identity in execution context — cannot access memory" };
|
|
283
287
|
if (!workflowId) return { error: "No workflow id in execution context — cannot access memory" };
|
|
284
288
|
|
|
@@ -296,6 +300,7 @@ function resolveIdentity(params: any, context: any): MemoryCallIdentity | { erro
|
|
|
296
300
|
return {
|
|
297
301
|
userId,
|
|
298
302
|
workflowId,
|
|
303
|
+
org,
|
|
299
304
|
agentId,
|
|
300
305
|
agentName,
|
|
301
306
|
accessToken: context?.auth?.accessToken,
|
|
@@ -328,7 +333,7 @@ async function memoryServerPost(path: string, body: any, accessToken?: string):
|
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
/**
|
|
331
|
-
* Serve search results LEAN (
|
|
336
|
+
* Serve search results LEAN (docs/architecture/signal/memory/MEMORY.md § Serving): claim + certainty + domain only —
|
|
332
337
|
* NO premises, NO content dupe, NO ids/timestamps/reinforcement bookkeeping.
|
|
333
338
|
* Deduped by normalized claim so paraphrase clusters read as one line. The raw
|
|
334
339
|
* retrieval is unchanged; the dashboard still gets full rows.
|
|
@@ -360,15 +365,21 @@ export async function executePlatformMemoryMethod(method: string, params: any, c
|
|
|
360
365
|
logger.warn(`Memory tool ${method} refused`, { error: identity.error });
|
|
361
366
|
return { success: false, method, error: identity.error };
|
|
362
367
|
}
|
|
363
|
-
const { userId, workflowId, agentId, agentName, accessToken, conversationId, executionId } = identity;
|
|
368
|
+
const { userId, workflowId, org, agentId, agentName, accessToken, conversationId, executionId } = identity;
|
|
364
369
|
|
|
365
370
|
try {
|
|
366
371
|
switch (method) {
|
|
367
372
|
// ---- user memory (the ONE depth tool; the standing snapshot is harness-injected) ----
|
|
368
373
|
case "queryMemory": {
|
|
374
|
+
// A person's memory is scoped by the org the run works as; a run that cannot name
|
|
375
|
+
// one gets no memory rather than someone else's (MEMORY.md § 2.10).
|
|
376
|
+
if (!org) {
|
|
377
|
+
logger.warn("Memory tool queryMemory refused: the run names no org");
|
|
378
|
+
return { success: false, method, error: "No org in execution context — cannot access memory" };
|
|
379
|
+
}
|
|
369
380
|
const evidence = await GetMemoryService.search({
|
|
370
381
|
userId,
|
|
371
|
-
|
|
382
|
+
org,
|
|
372
383
|
query: params?.query,
|
|
373
384
|
limit: 15,
|
|
374
385
|
accessToken,
|
|
@@ -379,7 +390,7 @@ export async function executePlatformMemoryMethod(method: string, params: any, c
|
|
|
379
390
|
|
|
380
391
|
// ---- agent memory (goal-scoped) ----
|
|
381
392
|
case "getGoalContext": {
|
|
382
|
-
return await memoryServerGet("/agent/goal-context", { userId, workflowId, agentId }, accessToken);
|
|
393
|
+
return await memoryServerGet("/agent/goal-context", { userId, workflowId, agentId, ...(org ? { org } : {}) }, accessToken);
|
|
383
394
|
}
|
|
384
395
|
case "writeNote": {
|
|
385
396
|
return await memoryServerPost(
|
|
@@ -387,6 +398,7 @@ export async function executePlatformMemoryMethod(method: string, params: any, c
|
|
|
387
398
|
{
|
|
388
399
|
userId,
|
|
389
400
|
workflowId,
|
|
401
|
+
org,
|
|
390
402
|
agentId,
|
|
391
403
|
agentName,
|
|
392
404
|
content: params?.content,
|
|
@@ -405,7 +417,8 @@ export async function executePlatformMemoryMethod(method: string, params: any, c
|
|
|
405
417
|
// because null means CLEAR.
|
|
406
418
|
const { description, acceptanceCriteria, directive, ...stateFields } = params || {};
|
|
407
419
|
const state = Object.keys(stateFields).length > 0 ? stateFields : undefined;
|
|
408
|
-
|
|
420
|
+
// `org` is the stamp on the goal at create (MEMORY.md § 2.10); identity stays the canvas.
|
|
421
|
+
const body: Record<string, any> = { userId, workflowId, org, agentId, agentName, description, acceptanceCriteria, state };
|
|
409
422
|
if (params && Object.prototype.hasOwnProperty.call(params, "directive")) body.directive = directive ?? null;
|
|
410
423
|
return await memoryServerPost("/agent/state", body, accessToken);
|
|
411
424
|
}
|
|
@@ -413,28 +426,28 @@ export async function executePlatformMemoryMethod(method: string, params: any, c
|
|
|
413
426
|
// The judge's verdict → one ledger row. Platform-only (PLATFORM_MEMORY_METHODS).
|
|
414
427
|
return await memoryServerPost(
|
|
415
428
|
"/agent/attempt",
|
|
416
|
-
{ userId, workflowId, agentId, results: params?.results, deliverables: params?.deliverables || [] },
|
|
429
|
+
{ userId, workflowId, org, agentId, results: params?.results, deliverables: params?.deliverables || [] },
|
|
417
430
|
accessToken,
|
|
418
431
|
);
|
|
419
432
|
}
|
|
420
433
|
case "searchHistory": {
|
|
421
434
|
return await memoryServerGet(
|
|
422
435
|
"/agent/history",
|
|
423
|
-
{ userId, workflowId, limit: String(params?.limit || 10) },
|
|
436
|
+
{ userId, workflowId, ...(org ? { org } : {}), limit: String(params?.limit || 10) },
|
|
424
437
|
accessToken,
|
|
425
438
|
);
|
|
426
439
|
}
|
|
427
440
|
case "resumeGoal": {
|
|
428
441
|
return await memoryServerPost(
|
|
429
442
|
"/agent/resume",
|
|
430
|
-
{ userId, workflowId, agentId, goalId: params?.goalId },
|
|
443
|
+
{ userId, workflowId, org, agentId, goalId: params?.goalId },
|
|
431
444
|
accessToken,
|
|
432
445
|
);
|
|
433
446
|
}
|
|
434
447
|
case "archiveGoal": {
|
|
435
448
|
return await memoryServerPost(
|
|
436
449
|
"/agent/archive",
|
|
437
|
-
{ userId, workflowId, agentId, summary: params?.summary, conversationId },
|
|
450
|
+
{ userId, workflowId, org, agentId, summary: params?.summary, conversationId },
|
|
438
451
|
accessToken,
|
|
439
452
|
);
|
|
440
453
|
}
|
package/vendor/sdk/appEngine.tsx
CHANGED
|
@@ -42,7 +42,7 @@ export interface AppConfig {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function AppEngine(cfg: AppConfig) {
|
|
45
|
-
const { serverUrl, apiUrl, appId, getAccessToken, userId, conversationId, surface, analyticsDebug } = cfg;
|
|
45
|
+
const { serverUrl, apiUrl, appId: configuredApp, getAccessToken, userId, conversationId, surface, analyticsDebug } = cfg;
|
|
46
46
|
|
|
47
47
|
// ONE STORE PER CONVERSATION. The host may reuse this engine (the native-host
|
|
48
48
|
// iframe posts new config on app switch), and a template swap keeps the store by
|
|
@@ -63,11 +63,28 @@ export function AppEngine(cfg: AppConfig) {
|
|
|
63
63
|
// Setting state during render makes React re-render BEFORE the commit, so the
|
|
64
64
|
// connection effect only ever sees either the old app intact or an empty binding —
|
|
65
65
|
// never the cross-app hybrid.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// WHICH APP IS RUNNING, and there are two ways it changes. The host re-posts its
|
|
67
|
+
// config (the native-host iframe's app switch), and the SERVER can now swap it mid
|
|
68
|
+
// conversation — an app discovered and called IS the surface, so opening one replaces
|
|
69
|
+
// what is mounted (owner ruling 2026-09-07). One piece of state either way, so the
|
|
70
|
+
// stale-binding guard below covers both rather than only the host's.
|
|
71
|
+
const [appId, setAppId] = useState(configuredApp);
|
|
72
|
+
const [boundFor, setBoundFor] = useState(configuredApp);
|
|
73
|
+
// The host's own switch WINS and resets: a new config is a new intent.
|
|
74
|
+
if (boundFor !== configuredApp) {
|
|
75
|
+
setBoundFor(configuredApp);
|
|
76
|
+
setAppId(configuredApp);
|
|
69
77
|
setBinding({});
|
|
70
78
|
}
|
|
79
|
+
// The server's switch, same reset: a different app is a different workflow and trigger,
|
|
80
|
+
// and the binding is read async from the incoming app's manifest (the effect below).
|
|
81
|
+
// Clearing it here is what stops a session opening on the OLD app's trigger against the
|
|
82
|
+
// new app — the hazard the render-time reset above exists for.
|
|
83
|
+
const switchApp = (next: string) => {
|
|
84
|
+
if (next === appId) return;
|
|
85
|
+
setAppId(next);
|
|
86
|
+
setBinding({});
|
|
87
|
+
};
|
|
71
88
|
// STATE-OWNED WIDTH (design data): the layout root's `appWidth` = the core surface's
|
|
72
89
|
// constant width; an occupied panel slot's `appWidth` adds to it (computeAppWidth).
|
|
73
90
|
// No declared width → the host keeps its own default. There is no other path.
|
|
@@ -177,11 +194,12 @@ export function AppEngine(cfg: AppConfig) {
|
|
|
177
194
|
userId: userId ?? "",
|
|
178
195
|
conversationId,
|
|
179
196
|
chatId: conversationId,
|
|
197
|
+
appId,
|
|
180
198
|
},
|
|
181
199
|
store,
|
|
182
200
|
// Open only once BOTH are ready: the user identity AND the manifest binding (read async).
|
|
183
201
|
// Avoids the transient "connection missing workflowId/userId/conversationId" warning.
|
|
184
|
-
{ enabled: !!userId && !!binding.workflow },
|
|
202
|
+
{ enabled: !!userId && !!binding.workflow, onAppSwitch: switchApp },
|
|
185
203
|
);
|
|
186
204
|
|
|
187
205
|
// STATUS TO THE HOST. In native-host mode the iframe owns the only real
|
|
@@ -294,14 +312,15 @@ export function AppEngine(cfg: AppConfig) {
|
|
|
294
312
|
store.setDraft(typeof (data as any)?.text === "string" ? (data as any).text : "");
|
|
295
313
|
return;
|
|
296
314
|
}
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
315
|
+
// THE SPEC'S ANSWERS, AND NOTHING ELSE (UNOVERSE_MCP_COMPLETE_GUIDE.md, "How the SDK
|
|
316
|
+
// answers an elicitation"): while the server is ELICITING on this session, the
|
|
317
|
+
// component's `submit` answers `accept` with its slice and its `cancel` answers
|
|
318
|
+
// `cancel`. Every other action (a state it enters, a step, a tab) is the component's
|
|
319
|
+
// own and goes to the server as usual. Without a pending elicitation, a submit falls
|
|
320
|
+
// back to the data-plane `user_action` waiter (a workflow node waiting on a component).
|
|
302
321
|
if (!t) return;
|
|
303
322
|
const state = (meta?.state ?? data) as Record<string, unknown>;
|
|
304
|
-
if (conn.resolveElicitation(state)) return;
|
|
323
|
+
if ((t === "submit" || t === "cancel") && conn.resolveElicitation(state, t === "cancel" ? "cancel" : "accept")) return;
|
|
305
324
|
conn.sendAction(t, {
|
|
306
325
|
componentNodeId: meta?.nodeId,
|
|
307
326
|
componentType: meta?.type,
|
|
@@ -18,6 +18,7 @@ import { dispatchAction, matches, type ComponentStore, type ResolvedTheme, type
|
|
|
18
18
|
import { renderNode, keyframesCss, type ActionHandler, type SlotResolver } from "./render";
|
|
19
19
|
import { useUnoverseTheme, ensureFontStylesheets } from "./theme";
|
|
20
20
|
import { IsolatedRoot } from "./isolate";
|
|
21
|
+
import { stateOfUri } from "./core/apps";
|
|
21
22
|
|
|
22
23
|
/** Subscribe to an instance in the store, re-rendering when a merge lands. Snapshots THIS
|
|
23
24
|
* instance's slice (merge() gives it a fresh identity), so chunks for other components bail
|
|
@@ -92,10 +93,13 @@ export function UnoverseComponent({ client, uri, data, store, chatId, nodeId, ex
|
|
|
92
93
|
// orgs share a name. The rebuild-from-type stays as the fallback for events that
|
|
93
94
|
// carry no componentUrl. (Still the plain URI, never the withdrawn `@refs` form —
|
|
94
95
|
// that suffix broke Studio's local definition route.)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
96
|
+
// THE HASH NAMES THE STATE (stateOfUri): the address is read without it, and it seeds the
|
|
97
|
+
// public `state` of a static instance that carries none, so `...#page` opens on the page.
|
|
98
|
+
const address = store ? (live.componentUrl ?? (live.type ? `unoverse://components/${live.type}` : null)) : (uri ?? null);
|
|
99
|
+
const { uri: resolvedUri, state: hashState } = address ? stateOfUri(address) : { uri: null as string | null };
|
|
100
|
+
const sourceData = store
|
|
101
|
+
? (extraData ? { ...(live.data ?? {}), ...extraData } : live.data)
|
|
102
|
+
: (hashState && data?.state == null ? { state: hashState, ...(data ?? {}) } : data);
|
|
99
103
|
// No theme passed → fetch it from the server (the SDK owns zero token values). The
|
|
100
104
|
// theme REF comes from the definition itself: a template's `org` names its client's
|
|
101
105
|
// theme (`<org>/light`); a universal component previews under the default set
|
package/vendor/sdk/lib/app.tsx
CHANGED
|
@@ -16,14 +16,15 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, use
|
|
|
16
16
|
import {
|
|
17
17
|
collectSurfacedViews,
|
|
18
18
|
cssWidth,
|
|
19
|
+
placeWidthStyle,
|
|
19
20
|
dispatchAction,
|
|
20
21
|
formatRelative,
|
|
21
22
|
isViewField,
|
|
23
|
+
mountAliases,
|
|
22
24
|
orderedClaims,
|
|
23
25
|
propDefaults,
|
|
24
26
|
resolveActiveLayout,
|
|
25
27
|
resolveActiveView,
|
|
26
|
-
resolveLatch,
|
|
27
28
|
selectPointers,
|
|
28
29
|
viewOf,
|
|
29
30
|
type ComponentStore,
|
|
@@ -254,6 +255,8 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
254
255
|
// guest's hand), it happens ONCE per instance (a placed slice has a view), and the
|
|
255
256
|
// component owns the axis from then on: the template never writes it again.
|
|
256
257
|
useEffect(() => {
|
|
258
|
+
// An app with places is drawn by the screen document: nothing is placed by a scan.
|
|
259
|
+
if (def?.places?.length) return;
|
|
257
260
|
const order = def?.stateOrder;
|
|
258
261
|
if (!order?.length) return;
|
|
259
262
|
for (const turn of store.getResponses())
|
|
@@ -294,6 +297,10 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
294
297
|
for (const secs of Object.values((tpl as { sections?: Record<string, { face?: string }[]> }).sections ?? {}))
|
|
295
298
|
for (const sec of secs) if (sec?.face) views.add(sec.face);
|
|
296
299
|
}
|
|
300
|
+
// `inline` IS NEVER A CLAIM: it is the universal fallback face, drawn in the flow. A
|
|
301
|
+
// streamed template's own `inline` layout (its compact chip) marked it claimed, so the
|
|
302
|
+
// flow skipped the chip (observed live 2026-09-06).
|
|
303
|
+
views.delete("inline");
|
|
297
304
|
return views;
|
|
298
305
|
}, [activeRoot, def, mounted]);
|
|
299
306
|
|
|
@@ -306,7 +313,11 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
306
313
|
// this stays a hook, and off the SAME derivation the render path uses.
|
|
307
314
|
const ladder = def ? resolveActiveView(def, store, surfacedViews) : { view: "", byGuest: false };
|
|
308
315
|
useEffect(() => {
|
|
309
|
-
if (
|
|
316
|
+
if (def?.places?.length) return;
|
|
317
|
+
if (ladder.view && !ladder.byGuest) {
|
|
318
|
+
const order = orderedClaims(def);
|
|
319
|
+
store.cancelBelow(ladder.view, order, mountAliases(def, new Set(order)));
|
|
320
|
+
}
|
|
310
321
|
// `structureVersion`: EVERY arrival re-runs this, not only the ones that change the
|
|
311
322
|
// winner. A card landing while a higher state is ALREADY active would otherwise keep
|
|
312
323
|
// its claim forever — the walk never re-ran — and closing the higher state dropped
|
|
@@ -315,52 +326,6 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
315
326
|
// only bumps when it changed something, so this settles in one extra pass.
|
|
316
327
|
}, [ladder.view, ladder.byGuest, def, store, structureVersion]);
|
|
317
328
|
|
|
318
|
-
/**
|
|
319
|
-
* THE LATCH, PUBLISHED FOR CHROME (docs/MCP_COMPLETE_GUIDE.md §The Component Latch).
|
|
320
|
-
*
|
|
321
|
-
* `resolveLatch` derives WHICH instance the conversation addresses; the composer's pill
|
|
322
|
-
* is ordinary design chrome, so it needs those facts as template state to bind against. Same
|
|
323
|
-
* shape as the voice service (§4 "services feed the state"): a producer writes derived
|
|
324
|
-
* values into the normal bucket, and `core` learns no UI concept — nothing here knows a
|
|
325
|
-
* pill exists.
|
|
326
|
-
*
|
|
327
|
-
* `latchDismissed` is written as bare `true` by the ✕ (chrome cannot name a key it does
|
|
328
|
-
* not know) and NORMALISED here to the dismissed holder's key. Keyed, so the dismissal
|
|
329
|
-
* dies with the thing dismissed: a different component taking the latch is a different
|
|
330
|
-
* key, and its pill comes up as it should rather than being suppressed by a stale flag.
|
|
331
|
-
*/
|
|
332
|
-
const tstate = store.getAppState() as Record<string, unknown>;
|
|
333
|
-
const dismissedRaw = tstate.latchDismissed;
|
|
334
|
-
const held = def ? resolveLatch(def, store, ladder.view, typeof dismissedRaw === "string" ? dismissedRaw : undefined) : null;
|
|
335
|
-
useEffect(() => {
|
|
336
|
-
if (dismissedRaw === true) {
|
|
337
|
-
// Resolve the bare flag against whoever holds it RIGHT NOW, then re-derive.
|
|
338
|
-
const current = def ? resolveLatch(def, store, ladder.view) : null;
|
|
339
|
-
store.mergeAppState({ latchDismissed: current ? current.key : "" });
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const l = held?.latch as { title?: unknown } | undefined;
|
|
343
|
-
const title = typeof l?.title === "string" ? l.title : "";
|
|
344
|
-
// `latched` is the DISCRIMINANT chrome branches on, exactly as the voice service
|
|
345
|
-
// publishes `callState`: ONE field for the axis, never a truthy title plus an inverse
|
|
346
|
-
// guard on its sibling, which is the boolean soup design/state.md rules out ("a case
|
|
347
|
-
// never re-guards its own discriminant"). Title and icon are the held case's DATA, not
|
|
348
|
-
// its condition — which is why they are separate keys and not the condition itself.
|
|
349
|
-
const latched = held != null;
|
|
350
|
-
if (tstate.latched !== latched || tstate.latchTitle !== title || tstate.latchKey !== (held?.key ?? ""))
|
|
351
|
-
store.mergeAppState({ latched, latchTitle: title, latchKey: held?.key ?? "" });
|
|
352
|
-
}, [
|
|
353
|
-
held?.key,
|
|
354
|
-
held?.latch,
|
|
355
|
-
dismissedRaw,
|
|
356
|
-
ladder.view,
|
|
357
|
-
def,
|
|
358
|
-
store,
|
|
359
|
-
tstate.latched,
|
|
360
|
-
tstate.latchTitle,
|
|
361
|
-
tstate.latchKey,
|
|
362
|
-
structureVersion,
|
|
363
|
-
]);
|
|
364
329
|
|
|
365
330
|
// Loading/error hints carry NO styling — the SDK owns zero styles.
|
|
366
331
|
if (error) return <div>Unoverse error: {error}</div>;
|
|
@@ -434,8 +399,54 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
434
399
|
// ComponentSlot — filter the timeline to matching component pointers (KeyService).
|
|
435
400
|
// A FACTORY: a mounted template's slots select within a SCOPE (the current
|
|
436
401
|
// delivery — hold: latest, basic form); the app's own slots stay unscoped.
|
|
402
|
+
// THE STATE THE SCREEN PLACED IT IN (docs/unoverse/design/screen/UNOVERSE_SCREEN.md §2.6): a place draws
|
|
403
|
+
// each interface in that face, so a guest's tap shows once the screen has placed it, and an open
|
|
404
|
+
// task can never draw in the conversation.
|
|
405
|
+
const placed = (p: string): Record<string, unknown> | undefined => {
|
|
406
|
+
const state = store.placedState(p);
|
|
407
|
+
return state ? { state } : undefined;
|
|
408
|
+
};
|
|
409
|
+
// WHAT A SLOT OR A PLACE HOLDS, drawn: its fallback when empty; else its frame around the
|
|
410
|
+
// leaves, a single occupant filling the frame; else the leaves as a list.
|
|
411
|
+
const drawPointers = (node: UnoverseNode, pointers: string[], key: React.Key | undefined, fill: boolean): ReactNode => {
|
|
412
|
+
if (pointers.length === 0) {
|
|
413
|
+
return node.fallback ? renderNode(node.fallback, {}, dispatch, theme, key ?? "fallback", undefined, def?.defs) : null;
|
|
414
|
+
}
|
|
415
|
+
// `frame` — chrome that exists ONLY because the slot matched (fallback's twin).
|
|
416
|
+
// The frame subtree renders with a slot resolver that serves the outer slot's
|
|
417
|
+
// matches to any bare nested ComponentSlot — so a focus overlay is BORN when a
|
|
418
|
+
// component enters the state and DIES when it leaves (§5b: derived, not stored).
|
|
419
|
+
if (node.frame) {
|
|
420
|
+
// A SURFACE'S SINGLE OCCUPANT FILLS THE SURFACE (docs/design/05): a limit-1
|
|
421
|
+
// surface (detail panel, focus overlay) gives its occupant the frame's full
|
|
422
|
+
// height — the component's `height: "full"` finally has a definite container,
|
|
423
|
+
// with zero per-face styling. Multi-occupant surfaces (a rail) stay content-sized.
|
|
424
|
+
// Multi-occupant surfaces (a rail) animate reorders; a single occupant fills its
|
|
425
|
+
// frame directly — a measuring wrapper there would break the height chain.
|
|
426
|
+
const innerSlot: SlotResolver = () =>
|
|
427
|
+
fill
|
|
428
|
+
? pointers.map((p) => leaf(p, placed(p), fill))
|
|
429
|
+
: <FlipList items={pointers.map((p) => ({ key: store.split(p).nodeId, node: leaf(p, placed(p)) }))} />;
|
|
430
|
+
// A panel declares its width ONCE (docs/design/05): `appWidth` grows the app
|
|
431
|
+
// (computeAppWidth) AND sizes the frame box here — the two can never disagree.
|
|
432
|
+
// Named sizes resolve via theme.appSize like any token; unresolvable → no width.
|
|
433
|
+
// `flex: 0 0 auto` — a panel is its declared width ALWAYS. A container that
|
|
434
|
+
// can't fit the sum clips at the edge; panels never compress into garbage.
|
|
435
|
+
// ONE WIDTH LAW (core/apps placeWidthStyle), the same as a template place and
|
|
436
|
+
// computeAppWidth. This frame once had its own copy without the `flex` word, and the
|
|
437
|
+
// task surface rendered in a zero-width frame beside a full-width app the moment the
|
|
438
|
+
// org's flex token went (SAB's comparison, blank, 2026-09-07).
|
|
439
|
+
const placeStyle = placeWidthStyle((node as { appWidth?: string }).appWidth, theme.appSize);
|
|
440
|
+
const frame = placeStyle ? ({ ...node.frame, style: { ...(node.frame.style ?? {}), ...placeStyle } } as UnoverseNode) : node.frame;
|
|
441
|
+
return renderNode(frame, store.getAppState() as Record<string, unknown>, dispatch, theme, key ?? "frame", innerSlot, def?.defs);
|
|
442
|
+
}
|
|
443
|
+
return <FlipList key={key ?? "slot"} items={pointers.map((p) => ({ key: store.split(p).nodeId, node: leaf(p, placed(p)) }))} />;
|
|
444
|
+
};
|
|
445
|
+
|
|
437
446
|
const makeResolveSlot = (scope?: Set<string>): SlotResolver => {
|
|
438
447
|
const resolveSlot: SlotResolver = (node: UnoverseNode, key?: React.Key): ReactNode => {
|
|
448
|
+
// A PLACE draws what the screen document put in it (docs/unoverse/design/screen/UNOVERSE_SCREEN.md).
|
|
449
|
+
if (node.type === "Place") return drawPointers(node, [...store.placeItems(String((node as { name?: string }).name ?? ""))], key, node.holds === "one");
|
|
439
450
|
// A TEMPLATE PLACE (the container noun folded into templates, 2026-08-28): the
|
|
440
451
|
// layout places it by NAME and owns its width; the manifest's `templates:` block
|
|
441
452
|
// owns what it holds. An entry with `use:` MOUNTS a template definition (its own
|
|
@@ -521,24 +532,17 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
521
532
|
// `flex` is the served "all remaining space" name. Without it the frame's
|
|
522
533
|
// own styles decide (and a width-less frame beside a fixed-width chat
|
|
523
534
|
// collapsed to its padding — observed live 2026-08-28).
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
// an unresolved name collapsed the place to its padding (observed live
|
|
529
|
-
// 2026-08-28, the light theme's missing `flex`).
|
|
530
|
-
const flexFallback = !placeW && declaredW === "flex";
|
|
535
|
+
// ONE WIDTH LAW (core/apps placeWidthStyle): a named size or raw CSS sizes the
|
|
536
|
+
// place; `flex` with no token is the SDK's own word, fill the remaining host space
|
|
537
|
+
// (an unresolved name once collapsed the place to its padding, 2026-08-28).
|
|
538
|
+
const placeStyle = placeWidthStyle((node as { appWidth?: string }).appWidth, theme.appSize);
|
|
531
539
|
const rawFrame = (node as { frame?: UnoverseNode }).frame;
|
|
532
540
|
if (rawFrame) {
|
|
533
|
-
const frame =
|
|
534
|
-
? ({ ...rawFrame, style: { ...(rawFrame.style ?? {}), width: placeW, flex: "0 0 auto" } } as UnoverseNode)
|
|
535
|
-
: flexFallback
|
|
536
|
-
? ({ ...rawFrame, style: { ...(rawFrame.style ?? {}), flex: "1 1 0%", minWidth: 0 } } as UnoverseNode)
|
|
537
|
-
: rawFrame;
|
|
541
|
+
const frame = placeStyle ? ({ ...rawFrame, style: { ...(rawFrame.style ?? {}), ...placeStyle } } as UnoverseNode) : rawFrame;
|
|
538
542
|
const innerSlot: SlotResolver = () => tree;
|
|
539
543
|
return renderNode(frame, store.getAppState() as Record<string, unknown>, dispatch, theme, `${String(key ?? cname)}-frame`, innerSlot, def?.defs);
|
|
540
544
|
}
|
|
541
|
-
return <div style={{ display: "flex", flexDirection: "column", minHeight: 0, ...(
|
|
545
|
+
return <div style={{ display: "flex", flexDirection: "column", minHeight: 0, ...((placeStyle ?? {}) as React.CSSProperties) }}>{tree}</div>;
|
|
542
546
|
}}
|
|
543
547
|
/>
|
|
544
548
|
);
|
|
@@ -589,39 +593,7 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
589
593
|
if (isViewField(claim?.field) && typeof claim?.eq === "string" && declaredClaim && claim.eq !== surfacedView) {
|
|
590
594
|
return node.fallback ? renderNode(node.fallback, {}, dispatch, theme, key ?? "fallback", undefined, def?.defs) : null;
|
|
591
595
|
}
|
|
592
|
-
|
|
593
|
-
if (pointers.length === 0) {
|
|
594
|
-
return node.fallback ? renderNode(node.fallback, {}, dispatch, theme, key ?? "fallback", undefined, def?.defs) : null;
|
|
595
|
-
}
|
|
596
|
-
// `frame` — chrome that exists ONLY because the slot matched (fallback's twin).
|
|
597
|
-
// The frame subtree renders with a slot resolver that serves the outer slot's
|
|
598
|
-
// matches to any bare nested ComponentSlot — so a focus overlay is BORN when a
|
|
599
|
-
// component enters the state and DIES when it leaves (§5b: derived, not stored).
|
|
600
|
-
if (node.frame) {
|
|
601
|
-
// A SURFACE'S SINGLE OCCUPANT FILLS THE SURFACE (docs/design/05): a limit-1
|
|
602
|
-
// surface (detail panel, focus overlay) gives its occupant the frame's full
|
|
603
|
-
// height — the component's `height: "full"` finally has a definite container,
|
|
604
|
-
// with zero per-face styling. Multi-occupant surfaces (a rail) stay content-sized.
|
|
605
|
-
const fill = node.select?.limit === 1;
|
|
606
|
-
// Multi-occupant surfaces (a rail) animate reorders; a single occupant fills its
|
|
607
|
-
// frame directly — a measuring wrapper there would break the height chain.
|
|
608
|
-
const innerSlot: SlotResolver = () =>
|
|
609
|
-
fill
|
|
610
|
-
? pointers.map((p) => leaf(p, undefined, fill))
|
|
611
|
-
: <FlipList items={pointers.map((p) => ({ key: store.split(p).nodeId, node: leaf(p) }))} />;
|
|
612
|
-
// A panel declares its width ONCE (docs/design/05): `appWidth` grows the app
|
|
613
|
-
// (computeAppWidth) AND sizes the frame box here — the two can never disagree.
|
|
614
|
-
// Named sizes resolve via theme.appSize like any token; unresolvable → no width.
|
|
615
|
-
// `flex: 0 0 auto` — a panel is its declared width ALWAYS. A container that
|
|
616
|
-
// can't fit the sum clips at the edge; panels never compress into garbage.
|
|
617
|
-
const declared = (node as { appWidth?: string }).appWidth;
|
|
618
|
-
const appWidth = cssWidth(typeof declared === "string" ? (theme.appSize?.[declared] ?? declared) : undefined);
|
|
619
|
-
const frame = appWidth
|
|
620
|
-
? ({ ...node.frame, style: { ...(node.frame.style ?? {}), width: appWidth, flex: "0 0 auto" } } as UnoverseNode)
|
|
621
|
-
: node.frame;
|
|
622
|
-
return renderNode(frame, store.getAppState() as Record<string, unknown>, dispatch, theme, key ?? "frame", innerSlot, def?.defs);
|
|
623
|
-
}
|
|
624
|
-
return <FlipList key={key ?? "slot"} items={pointers.map((p) => ({ key: store.split(p).nodeId, node: leaf(p) }))} />;
|
|
596
|
+
return drawPointers(node, selectPointers(store, node, scope), key, node.select?.limit === 1);
|
|
625
597
|
};
|
|
626
598
|
return resolveSlot;
|
|
627
599
|
};
|
|
@@ -632,6 +604,8 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
632
604
|
// in those sub-trees (design/templates/chatlayout/*.json). The assistant sub-tree's ComponentSlot
|
|
633
605
|
// is scoped to THAT turn's components.
|
|
634
606
|
const resolveTimeline = (node: UnoverseNode, key?: React.Key): ReactNode => {
|
|
607
|
+
// An app with places shows in the conversation exactly what the screen put there.
|
|
608
|
+
const inChat = def.places?.length ? new Set(store.placeItems("chat")) : null;
|
|
635
609
|
return (
|
|
636
610
|
<div key={key} style={styleToCss(node.style, theme)}>
|
|
637
611
|
{store.getTimeline().map((turn) => {
|
|
@@ -645,6 +619,7 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
645
619
|
const shown =
|
|
646
620
|
turn.role === "assistant"
|
|
647
621
|
? turn.components.filter((p) => {
|
|
622
|
+
if (inChat) return inChat.has(p);
|
|
648
623
|
const { chatId, nodeId } = store.split(p);
|
|
649
624
|
const view = viewOf(store.get(chatId, nodeId) as Record<string, unknown> | undefined);
|
|
650
625
|
return !view || !surfacedViews.has(view);
|
|
@@ -658,7 +633,7 @@ export function StreamedUnoverseApp({ client, store, uri, onAction, theme: theme
|
|
|
658
633
|
// Pass the turn's streaming state into each component's scope so a streamed
|
|
659
634
|
// component (e.g. StreamingText) can stop its own loading dots when complete.
|
|
660
635
|
const streaming = turn.role === "assistant" && turn.streamingState === "streaming";
|
|
661
|
-
return ptrs.map((p) => leaf(p, { streaming }));
|
|
636
|
+
return ptrs.map((p) => leaf(p, { streaming, ...placed(p) }));
|
|
662
637
|
};
|
|
663
638
|
// Data scope per turn = author's static data (avatarUrl, thinkingText…)
|
|
664
639
|
// + NEUTRAL projections of the timeline model. The SDK exposes facts, never
|