@unoverse-platform/studio 0.1.96 → 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 +3 -3
- package/local/authoring.ts +1 -1
- package/local/catalog.ts +22 -2
- package/local/keys.ts +1 -1
- package/local/nodes.ts +29 -42
- package/local/plugin.ts +75 -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 +51 -22
- package/local/source.ts +4 -9
- 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/IdentityHeader.tsx +1 -1
- package/screens/IdentityView.tsx +206 -0
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/SourceView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +5 -2
- package/src/App.tsx +14 -4
- package/src/NodeKeys.tsx +1 -1
- package/src/host.ts +3 -16
- 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 +107 -0
- package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
- 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 +33 -25
- package/vendor/base/src/items/publish.ts +31 -7
- 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 +6 -3
- 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 +6 -1
- 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 +81 -10
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +12 -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/index.ts +1 -1
- 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/manifests/source.ts +1 -1
- 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/README.md +1 -1
- 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 +31 -12
- 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/vite.config.ts +1 -1
package/local/source.ts
CHANGED
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
* install, and §Design-system resolution is what keeps the token contract stable.
|
|
15
15
|
*
|
|
16
16
|
* So the writable set is exactly the developer's own work:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* nodes/** their own node packages
|
|
17
|
+
* design/<project>/** every project listOrgs() reports, via projectDir(); their own
|
|
18
|
+
* node packages live inside it (design/<project>/nodes/)
|
|
20
19
|
* Everything else is readable and not writable, the marketplace and anything resolved out
|
|
21
20
|
* of node_modules included.
|
|
22
21
|
*
|
|
23
|
-
* See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
|
|
22
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
|
|
24
23
|
*/
|
|
25
24
|
import { designHome } from "./design-home.mjs";
|
|
26
25
|
import { resolve, sep, relative } from "path";
|
|
@@ -51,11 +50,7 @@ export function writableRoots(
|
|
|
51
50
|
projectRoot: string,
|
|
52
51
|
loaders: { listOrgs: () => string[]; projectDir: (name: string) => string },
|
|
53
52
|
): string[] {
|
|
54
|
-
const roots =
|
|
55
|
-
...loaders.listOrgs().map((org) => loaders.projectDir(org)),
|
|
56
|
-
resolve(projectRoot, "prompts"),
|
|
57
|
-
resolve(projectRoot, "nodes"),
|
|
58
|
-
];
|
|
53
|
+
const roots = loaders.listOrgs().map((org) => loaders.projectDir(org));
|
|
59
54
|
return roots.map((r) => resolve(r)).filter((r) => existsSync(r));
|
|
60
55
|
}
|
|
61
56
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents view — the Agents a project authored (docs.unoverse.ai/design/tasks). Left: a
|
|
3
|
+
* navigator. Right: the selected Agent's meta, its door, and the Available switch.
|
|
4
|
+
*
|
|
5
|
+
* An Agent is a workflow with no interface of its own, listed on the map so a conversation
|
|
6
|
+
* can reach for it: the one thing authored for the map alone (`design/<org>/agents/<name>.yaml`).
|
|
7
|
+
* Every other asset (a component, a template, an app) keeps its meta on its own manifest and
|
|
8
|
+
* its switch on its own lane. Switched on, all of them are tasks on the map.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
|
+
import { fetchUnoverse } from "studio-host";
|
|
12
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
13
|
+
import { IdentityHeader } from "./IdentityHeader";
|
|
14
|
+
import { DOC, EMPTY, NAV, NAV_SECTION, PANE, SCREEN, navItem } from "./chrome";
|
|
15
|
+
import { EmptyLibrary } from "./EmptyLibrary";
|
|
16
|
+
|
|
17
|
+
interface AgentItem {
|
|
18
|
+
/** `<org>/<name>`: the identity every registry row and toggle keys on. */
|
|
19
|
+
name: string;
|
|
20
|
+
org: string;
|
|
21
|
+
id: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
whenToUse?: string;
|
|
25
|
+
instructions?: string;
|
|
26
|
+
skills?: string[];
|
|
27
|
+
category?: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
/** The face while it runs. */
|
|
31
|
+
app: string;
|
|
32
|
+
/** The door. */
|
|
33
|
+
workflow?: string;
|
|
34
|
+
trigger?: string;
|
|
35
|
+
output?: string;
|
|
36
|
+
path: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ACCENT = "#7c3aed";
|
|
40
|
+
/** An Agent's mark: a bot's head, antenna up, two eyes. */
|
|
41
|
+
const AGENT_ICON = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${ACCENT}" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="9" width="16" height="11" rx="3"/><path d="M12 9V5"/><circle cx="12" cy="4" r="1.2" fill="${ACCENT}"/><circle cx="9" cy="14.5" r="1.3" fill="${ACCENT}" stroke="none"/><circle cx="15" cy="14.5" r="1.3" fill="${ACCENT}" stroke="none"/><path d="M2 13v4M22 13v4"/></svg>`)}`;
|
|
42
|
+
|
|
43
|
+
export function AgentsView({ org = "all" }: { org?: string } = {}) {
|
|
44
|
+
const [agents, setAgents] = useState<AgentItem[]>([]);
|
|
45
|
+
const available = useRegistryToggles("agent");
|
|
46
|
+
const [selected, setSelected] = usePersistentState<string | null>("wb:agents:selected", null);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
fetchUnoverse(`/dev/agents${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`)
|
|
51
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
52
|
+
.then((d) => {
|
|
53
|
+
const list: AgentItem[] = d.items ?? [];
|
|
54
|
+
setAgents(list);
|
|
55
|
+
const restore = list.find((a) => a.name === selected) ?? list[0];
|
|
56
|
+
setSelected(restore ? restore.name : null);
|
|
57
|
+
})
|
|
58
|
+
.catch((e) => setError(String(e?.message ?? e)));
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
+
}, [org]);
|
|
61
|
+
|
|
62
|
+
const detail = agents.find((a) => a.name === selected) ?? null;
|
|
63
|
+
|
|
64
|
+
const byCategory = Object.entries(
|
|
65
|
+
agents.reduce<Record<string, AgentItem[]>>((acc, a) => {
|
|
66
|
+
(acc[a.category ?? "Uncategorized"] ??= []).push(a);
|
|
67
|
+
return acc;
|
|
68
|
+
}, {}),
|
|
69
|
+
).sort(([a], [b]) => a.localeCompare(b));
|
|
70
|
+
|
|
71
|
+
if (agents.length === 0) {
|
|
72
|
+
return (
|
|
73
|
+
<div className={SCREEN}>
|
|
74
|
+
<nav className={NAV} />
|
|
75
|
+
<EmptyLibrary
|
|
76
|
+
noun="agents"
|
|
77
|
+
org={org}
|
|
78
|
+
folder="agents"
|
|
79
|
+
shape="An Agent is one YAML file: a workflow with no interface, listed for the map with its selection text and its door (a canvas, its Input Trigger, the output node)."
|
|
80
|
+
error={error}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={SCREEN}>
|
|
88
|
+
{/* NAVIGATOR */}
|
|
89
|
+
<nav className={NAV}>
|
|
90
|
+
{byCategory.map(([cat, items]) => (
|
|
91
|
+
<div key={cat} className="mb-3">
|
|
92
|
+
<div className={NAV_SECTION}>{cat}</div>
|
|
93
|
+
{items.map((a) => (
|
|
94
|
+
<button
|
|
95
|
+
key={a.name}
|
|
96
|
+
onClick={() => setSelected(a.name)}
|
|
97
|
+
className={`block w-full rounded px-2 py-1.5 text-left transition ${navItem(selected === a.name)}`}
|
|
98
|
+
>
|
|
99
|
+
<div className="flex min-w-0 items-baseline gap-2 text-sm font-medium">
|
|
100
|
+
<span className="min-w-0 flex-1 truncate">{a.title ?? a.id}</span>
|
|
101
|
+
<span className="shrink-0 text-[10px] text-gray-500">{a.org}</span>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="truncate text-[11px] text-gray-500">{a.description}</div>
|
|
104
|
+
</button>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</nav>
|
|
109
|
+
|
|
110
|
+
{/* DETAIL */}
|
|
111
|
+
<div className={PANE}>
|
|
112
|
+
{!selected || !detail ? (
|
|
113
|
+
<div className={EMPTY}>Select an Agent</div>
|
|
114
|
+
) : (
|
|
115
|
+
<div className={DOC}>
|
|
116
|
+
{/* AVAILABLE — the master switch. Which canvases actually carry this Agent is
|
|
117
|
+
installed per workflow, in Canvas's Content Library. */}
|
|
118
|
+
<IdentityHeader
|
|
119
|
+
right={<ToggleSwitch on={available.isEnabled(selected)} onClick={() => available.toggle(selected)} label="Available" />}
|
|
120
|
+
logoUrl={AGENT_ICON}
|
|
121
|
+
accent={ACCENT}
|
|
122
|
+
name={selected}
|
|
123
|
+
category={detail.category}
|
|
124
|
+
version={detail.version}
|
|
125
|
+
description={detail.description}
|
|
126
|
+
whenToUse={detail.whenToUse}
|
|
127
|
+
/>
|
|
128
|
+
<p className="mb-3 text-[11px] text-gray-500">Available means canvases may install this Agent. Which ones actually have it is chosen per workflow, in Canvas’s Content Library.</p>
|
|
129
|
+
|
|
130
|
+
{detail.instructions && (
|
|
131
|
+
<>
|
|
132
|
+
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">What to do</div>
|
|
133
|
+
<pre className="mb-5 overflow-x-auto whitespace-pre-wrap rounded-lg border border-gray-200 bg-gray-50 p-4 font-mono text-[12.5px] leading-relaxed text-gray-800">{detail.instructions}</pre>
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
{detail.skills && detail.skills.length > 0 && (
|
|
137
|
+
<>
|
|
138
|
+
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">Skills required</div>
|
|
139
|
+
<div className="mb-5 flex flex-wrap gap-1.5">
|
|
140
|
+
{detail.skills.map((s) => (
|
|
141
|
+
<span key={s} className="rounded-full bg-gray-900/5 px-2 py-0.5 text-[11px] text-gray-600">{s}</span>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">Door</div>
|
|
148
|
+
<dl className="mb-5 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg border border-gray-200 bg-gray-50 p-4 font-mono text-[12px] text-gray-800">
|
|
149
|
+
<dt className="text-gray-500">workflow</dt>
|
|
150
|
+
<dd>{detail.workflow}</dd>
|
|
151
|
+
<dt className="text-gray-500">trigger</dt>
|
|
152
|
+
<dd>{detail.trigger}</dd>
|
|
153
|
+
<dt className="text-gray-500">output</dt>
|
|
154
|
+
<dd>{detail.output}</dd>
|
|
155
|
+
<dt className="text-gray-500">face</dt>
|
|
156
|
+
<dd className="break-all">{detail.app}</dd>
|
|
157
|
+
{detail.message && (
|
|
158
|
+
<>
|
|
159
|
+
<dt className="text-gray-500">message</dt>
|
|
160
|
+
<dd>{detail.message}</dd>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
<dt className="text-gray-500">input schema</dt>
|
|
164
|
+
<dd>the Input Trigger's, on the canvas</dd>
|
|
165
|
+
<dt className="text-gray-500">file</dt>
|
|
166
|
+
<dd>{detail.path}</dd>
|
|
167
|
+
</dl>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -613,9 +613,9 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
613
613
|
color: metaOpen ? theme.color["text.primary"] : theme.color["text.secondary"],
|
|
614
614
|
background: metaOpen ? theme.color["surface.sunken"] : "transparent",
|
|
615
615
|
}}
|
|
616
|
-
title="Show the component's manifest
|
|
616
|
+
title="Show the component's manifest: its meta and its arrival"
|
|
617
617
|
>
|
|
618
|
-
ⓘ
|
|
618
|
+
ⓘ Manifest <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
|
|
619
619
|
</button>
|
|
620
620
|
)}
|
|
621
621
|
|
package/screens/EmptyLibrary.tsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* without needing to say so.
|
|
12
12
|
*
|
|
13
13
|
* AND IT MUST NAME THE RIGHT FOLDER. The old copy always said
|
|
14
|
-
* `apps/unoverse/
|
|
14
|
+
* `apps/unoverse/design/marketplace/blocks/`, which is the UNIVERSAL tier. Studio scoped to an org
|
|
15
15
|
* is telling you where to author for THAT org, and sending someone to the universal
|
|
16
16
|
* folder would have them write a platform-wide asset when they wanted their own.
|
|
17
17
|
*/
|
|
@@ -75,7 +75,7 @@ export function IdentityHeader({
|
|
|
75
75
|
{id && <div className="mt-0.5 font-mono text-[11px] text-gray-400">{id}</div>}
|
|
76
76
|
{description && <p className="mt-1 max-w-3xl text-sm leading-relaxed text-gray-600">{description}</p>}
|
|
77
77
|
{whenToUse && (
|
|
78
|
-
<div className="mt-2 max-w-3xl rounded-
|
|
78
|
+
<div className="mt-2 max-w-3xl rounded-lg px-3 py-2" style={{ background: "#f3f4f6" }}>
|
|
79
79
|
<div className="mb-0.5 text-[10px] font-semibold uppercase tracking-wider" style={{ color: accent }}>
|
|
80
80
|
When to use
|
|
81
81
|
</div>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity view — the org's four documents (organisation, brand, purpose, story) as the
|
|
3
|
+
* pack holds them: design/<org>/identity/<doc>.yaml. Left: a navigator, one group per org
|
|
4
|
+
* (or the one org this Studio is bound to). Right: the selected document's manifest words,
|
|
5
|
+
* then each prop with its brief and its default, the org's own words.
|
|
6
|
+
*
|
|
7
|
+
* READ-ONLY, like Skills: the documents are authored in the pack (the create skill fills
|
|
8
|
+
* them from the org's own sources) and published with it. The one control is the same
|
|
9
|
+
* master switch every other kind has: Available puts a document in the spatial catalogue,
|
|
10
|
+
* and each canvas installs it from there. The Organisation's switch is the grounding: on,
|
|
11
|
+
* every run as this org wakes with it; it never sits on the map
|
|
12
|
+
* (docs/unoverse/data/objects/elements/organisation.md).
|
|
13
|
+
*
|
|
14
|
+
* Shared by both Studios (packages/studio/screens); the host supplies fetch + toggles.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useMemo, useState } from "react";
|
|
17
|
+
import { fetchUnoverse } from "studio-host";
|
|
18
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
19
|
+
import { IdentityHeader } from "./IdentityHeader";
|
|
20
|
+
import { DOC, EMPTY, NAV, NAV_SECTION, PANE, SCREEN, navItem } from "./chrome";
|
|
21
|
+
import { EmptyLibrary } from "./EmptyLibrary";
|
|
22
|
+
|
|
23
|
+
interface IdentityProp {
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
maxLength?: number;
|
|
28
|
+
maxItems?: number;
|
|
29
|
+
default: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface IdentityItem {
|
|
33
|
+
/** The registry name, `<org>/<doc>`: the same word every other surface addresses it by. */
|
|
34
|
+
name: string;
|
|
35
|
+
org: string;
|
|
36
|
+
doc: "organisation" | "brand" | "purpose" | "story";
|
|
37
|
+
manifest: { title?: string; description?: string; whenToUse?: string; category?: string; version?: string };
|
|
38
|
+
props: IdentityProp[];
|
|
39
|
+
path: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ORDER = ["organisation", "brand", "purpose", "story"] as const;
|
|
43
|
+
const isEmpty = (v: unknown): boolean => v == null || (typeof v === "string" && !v.trim()) || (Array.isArray(v) && v.length === 0);
|
|
44
|
+
|
|
45
|
+
function Value({ value }: { value: unknown }) {
|
|
46
|
+
if (isEmpty(value)) return <div className="rounded-lg border border-dashed border-gray-200 px-4 py-3 text-[13px] italic text-gray-400">Not written yet.</div>;
|
|
47
|
+
if (typeof value === "string") return <p className="whitespace-pre-wrap text-[15px] leading-7 text-gray-900">{value}</p>;
|
|
48
|
+
if (Array.isArray(value)) return <Tree items={value} />;
|
|
49
|
+
return <pre className="rounded-lg bg-gray-50 p-3 text-xs text-gray-700">{JSON.stringify(value, null, 2)}</pre>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Tree({ items, depth = 0 }: { items: unknown[]; depth?: number }) {
|
|
53
|
+
if (items.every((it) => typeof it === "string")) {
|
|
54
|
+
return (
|
|
55
|
+
<ul className="space-y-2">
|
|
56
|
+
{items.map((it, i) => (
|
|
57
|
+
<li key={i} className="flex gap-3 text-[15px] leading-7 text-gray-900">
|
|
58
|
+
<span className="mt-[11px] h-1.5 w-1.5 shrink-0 rounded-full bg-gray-300" />
|
|
59
|
+
<span>{String(it)}</span>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<div className={depth ? "ml-5 mt-2" : "divide-y divide-gray-100 rounded-xl border border-gray-200"}>
|
|
67
|
+
{items.map((it, i) => {
|
|
68
|
+
const o = (it ?? {}) as Record<string, unknown>;
|
|
69
|
+
const kids = (o.units ?? o.goals ?? o.brands) as unknown[] | undefined;
|
|
70
|
+
const sentence = (o.what ?? o.why) as string | undefined;
|
|
71
|
+
return (
|
|
72
|
+
<div key={i} className={depth ? "py-2" : "px-4 py-3"}>
|
|
73
|
+
<div className="text-[14px] font-semibold text-gray-900">{String(o.name ?? "")}</div>
|
|
74
|
+
{sentence ? <div className="mt-0.5 text-[13.5px] leading-6 text-gray-600">{sentence}</div> : null}
|
|
75
|
+
{Array.isArray(kids) && kids.length > 0 ? <Tree items={kids} depth={depth + 1} /> : null}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One prop: its name, the value set as reading text, and the brief behind a small toggle. */
|
|
84
|
+
function Field({ prop }: { prop: IdentityProp }) {
|
|
85
|
+
const [brief, setBrief] = useState(false);
|
|
86
|
+
const limit = prop.maxItems ? `up to ${prop.maxItems}` : prop.maxLength ? `up to ${prop.maxLength} characters` : "";
|
|
87
|
+
return (
|
|
88
|
+
<section className="py-6 first:pt-0 last:pb-0">
|
|
89
|
+
<div className="mb-2 flex items-baseline justify-between gap-4">
|
|
90
|
+
<h3 className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">{prop.name}</h3>
|
|
91
|
+
{prop.description ? (
|
|
92
|
+
<button type="button" onClick={() => setBrief((b) => !b)} className={`text-[11px] font-medium transition ${brief ? "text-indigo-600" : "text-gray-400 hover:text-gray-600"}`}>
|
|
93
|
+
{brief ? "Hide the brief" : "Brief"}
|
|
94
|
+
</button>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
{brief && prop.description ? (
|
|
98
|
+
<div className="mb-3 rounded-lg border border-indigo-100 bg-indigo-50/60 px-4 py-3 text-[12.5px] leading-relaxed text-indigo-900">
|
|
99
|
+
{prop.description}
|
|
100
|
+
{limit ? <div className="mt-1.5 text-[11px] text-indigo-500">{limit}</div> : null}
|
|
101
|
+
</div>
|
|
102
|
+
) : null}
|
|
103
|
+
<Value value={prop.default} />
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function IdentityView({ org = "all" }: { org?: string } = {}) {
|
|
109
|
+
const [items, setItems] = useState<IdentityItem[]>([]);
|
|
110
|
+
const [error, setError] = useState<string | null>(null);
|
|
111
|
+
const [selected, setSelected] = usePersistentState<string | null>("wb:identity:selected", null);
|
|
112
|
+
const available = useRegistryToggles("identity");
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
fetchUnoverse(`/dev/identity${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`)
|
|
116
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
117
|
+
.then((d) => {
|
|
118
|
+
const list: IdentityItem[] = d.items ?? [];
|
|
119
|
+
setItems(list);
|
|
120
|
+
const restore = list.find((i) => i.name === selected) ?? list[0];
|
|
121
|
+
setSelected(restore ? restore.name : null);
|
|
122
|
+
})
|
|
123
|
+
.catch((e) => setError(String(e?.message ?? e)));
|
|
124
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
125
|
+
}, [org]);
|
|
126
|
+
|
|
127
|
+
const byOrg = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
Object.entries(
|
|
130
|
+
items.reduce<Record<string, IdentityItem[]>>((acc, i) => {
|
|
131
|
+
(acc[i.org] ??= []).push(i);
|
|
132
|
+
return acc;
|
|
133
|
+
}, {}),
|
|
134
|
+
)
|
|
135
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
136
|
+
.map(([o, docs]) => [o, [...docs].sort((a, b) => ORDER.indexOf(a.doc) - ORDER.indexOf(b.doc))] as const),
|
|
137
|
+
[items],
|
|
138
|
+
);
|
|
139
|
+
const current = items.find((i) => i.name === selected) ?? null;
|
|
140
|
+
|
|
141
|
+
if (items.length === 0) {
|
|
142
|
+
return (
|
|
143
|
+
<div className={SCREEN}>
|
|
144
|
+
<nav className={NAV} />
|
|
145
|
+
<EmptyLibrary
|
|
146
|
+
noun="identity documents"
|
|
147
|
+
org={org}
|
|
148
|
+
folder="identity"
|
|
149
|
+
shape="identity/ holds four files: organisation, brand, purpose and story, each an instance of its marketplace object, carrying the org's own words as defaults. The briefs are the object's."
|
|
150
|
+
error={error}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={SCREEN}>
|
|
158
|
+
<nav className={NAV}>
|
|
159
|
+
{byOrg.map(([o, docs]) => (
|
|
160
|
+
<div key={o} className="mb-3">
|
|
161
|
+
<div className={NAV_SECTION}>{o}</div>
|
|
162
|
+
{docs.map((d) => (
|
|
163
|
+
<button key={d.name} onClick={() => setSelected(d.name)} className={`block w-full rounded px-2 py-1.5 text-left transition ${navItem(selected === d.name)}`}>
|
|
164
|
+
<div className="text-sm font-medium">{d.manifest.title ?? d.doc}</div>
|
|
165
|
+
<div className="truncate text-[11px] text-gray-500">{d.manifest.description}</div>
|
|
166
|
+
</button>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</nav>
|
|
171
|
+
|
|
172
|
+
<div className={PANE}>
|
|
173
|
+
{!current ? (
|
|
174
|
+
<div className={EMPTY}>Select a document</div>
|
|
175
|
+
) : (
|
|
176
|
+
<div className={DOC}>
|
|
177
|
+
<IdentityHeader
|
|
178
|
+
name={current.name}
|
|
179
|
+
category={current.manifest.category}
|
|
180
|
+
version={current.manifest.version}
|
|
181
|
+
id={`design/${current.org}/${current.path}`}
|
|
182
|
+
description={current.manifest.description}
|
|
183
|
+
whenToUse={current.manifest.whenToUse}
|
|
184
|
+
right={<ToggleSwitch on={available.isEnabled(current.name)} onClick={() => available.toggle(current.name)} label={current.doc === "organisation" ? "Grounding" : "Available"} />}
|
|
185
|
+
/>
|
|
186
|
+
<p className="mb-6 text-[12px] leading-relaxed text-gray-500">
|
|
187
|
+
{current.doc === "organisation"
|
|
188
|
+
? "Grounding on means every extraction and every Agent that runs as this org reads this document whole: its what and its vocabulary. Off, the org grounds on nothing."
|
|
189
|
+
: "Available puts this document in the spatial catalogue. Which maps carry it is chosen per canvas, in Canvas’s Content Library."}
|
|
190
|
+
{" "}Written in the pack; the create skill fills it from the org’s own sources.
|
|
191
|
+
</p>
|
|
192
|
+
<p className="mb-6 text-[11px] text-gray-500">
|
|
193
|
+
Reference in a prompt as <span className="font-mono text-gray-600">{`{{identity.${current.doc}}}`}</span> for the whole document
|
|
194
|
+
{current.props[0] ? <>, or one field as <span className="font-mono text-gray-600">{`{{identity.${current.doc}.${current.props[0].name}}}`}</span></> : null}.
|
|
195
|
+
Every document at once is <span className="font-mono text-gray-600">{"{{identity}}"}</span>.
|
|
196
|
+
</p>
|
|
197
|
+
|
|
198
|
+
<div className="divide-y divide-gray-100">
|
|
199
|
+
{current.props.map((p) => <Field key={p.name} prop={p} />)}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prompt-blocks view — browse the reusable prompt-snippet library
|
|
3
|
-
* (apps/unoverse/
|
|
3
|
+
* (apps/unoverse/design/marketplace/blocks) served by the Unoverse server's /prompt-blocks
|
|
4
4
|
* route. Left: a navigator of blocks grouped by category. Right: the selected block's
|
|
5
5
|
* metadata + full markdown content, with an enable toggle.
|
|
6
6
|
*
|
package/screens/SourceView.tsx
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* lost on the next install, and the design system is what keeps the token contract stable.
|
|
20
20
|
* The verdict is the host's, never a rule reimplemented here.
|
|
21
21
|
*
|
|
22
|
-
* See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
|
|
22
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
|
|
23
23
|
*/
|
|
24
24
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
25
25
|
import { Compartment, EditorState } from "@codemirror/state";
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* NOT TemplatesView.tsx: that name is taken by the Apps screen (the legacy noun).
|
|
18
18
|
*/
|
|
19
19
|
import { useEffect, useMemo, useState, useSyncExternalStore, useCallback, type MouseEvent as ReactMouseEvent } from "react";
|
|
20
|
-
import { ComponentStore,
|
|
20
|
+
import { ComponentStore, UnoverseComponent, type ResolvedTheme } from "@unoverse/sdk";
|
|
21
21
|
import { client, authedFetch, usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
22
22
|
import { NAV, SCREEN, navItem } from "./chrome";
|
|
23
23
|
|
|
@@ -68,6 +68,13 @@ interface TemplateDef {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const PREVIEW_CHAT = "preview";
|
|
71
|
+
/** THE TEMPLATE ITSELF, as ONE INSTANCE (TEMPLATE_MODEL §5d, ruled 2026-09-06). A template
|
|
72
|
+
* that streams in is an instance that draws itself: its ✕ and its inline chip write ITS
|
|
73
|
+
* state, and its root Switch draws the layout that face names. The preview used to render
|
|
74
|
+
* the template as an APP over the store, where those writes have no slice and are inert
|
|
75
|
+
* (app.tsx: "template chrome has NO component slice"). Now it renders the same instance
|
|
76
|
+
* the runtime renders. */
|
|
77
|
+
const TEMPLATE_NODE = "~template";
|
|
71
78
|
|
|
72
79
|
const VIEWPORTS: { id: string; label: string; w: number | null }[] = [
|
|
73
80
|
{ id: "mobile", label: "Mobile", w: 390 },
|
|
@@ -155,6 +162,31 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
155
162
|
const current = templates.find((t) => refOf(t) === selected);
|
|
156
163
|
const uri = useMemo(() => (current ? `unoverse://templates/${refOf(current)}` : ""), [current]);
|
|
157
164
|
|
|
165
|
+
// COPY FOR CANVAS — the same clipboard payload ComponentsView writes (gravity-workflow,
|
|
166
|
+
// the shape the canvas's own Cmd+C emits), so Cmd+V on any canvas pastes this template as
|
|
167
|
+
// the ONE generic `Component` node. The server synthesizes templates into that node beside
|
|
168
|
+
// the components (runtime/components/meta.ts, "templates synthesize too", §9c): selected by
|
|
169
|
+
// `config.component`, rendered at its own home (unoverse://templates/<ref>) through the
|
|
170
|
+
// node's componentUrls map, and filled through the node's MCP port — an Agent wired to it
|
|
171
|
+
// reads the briefs and fills every `input: true` part. Identity only in config, never the
|
|
172
|
+
// mock values: the fills come from the delivery, and a copied mock would freeze into the
|
|
173
|
+
// workflow. Width 768, the tablet stage this view designs at; height hugs the render.
|
|
174
|
+
const [copiedForCanvas, setCopiedForCanvas] = useState(false);
|
|
175
|
+
const copyForCanvas = async () => {
|
|
176
|
+
if (!current) return;
|
|
177
|
+
const node = {
|
|
178
|
+
id: `${current.name.toLowerCase()}1`,
|
|
179
|
+
type: "Component",
|
|
180
|
+
position: { x: 0, y: 0 },
|
|
181
|
+
data: { label: current.title ?? current.name, status: "ready", config: { component: refOf(current) } },
|
|
182
|
+
style: { width: 768 },
|
|
183
|
+
draggable: true,
|
|
184
|
+
};
|
|
185
|
+
await navigator.clipboard.writeText(JSON.stringify({ type: "gravity-workflow", version: 2, nodes: [node], edges: [] }, null, 2));
|
|
186
|
+
setCopiedForCanvas(true);
|
|
187
|
+
setTimeout(() => setCopiedForCanvas(false), 1600);
|
|
188
|
+
};
|
|
189
|
+
|
|
158
190
|
// REAL INSTANCES, fresh per selection (the Components screen's own pattern): the
|
|
159
191
|
// manifest's preview refs seed the store, and from there the standard runtime owns
|
|
160
192
|
// everything — a tap re-faces an instance, the name-matched layout surfaces, its
|
|
@@ -199,6 +231,17 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
199
231
|
store.apply({ type: "COMPONENT_INIT", chatId: PREVIEW_CHAT, nodeId, component: { type: ref, props: seed ?? {} } });
|
|
200
232
|
if (face) store.mergeComponentState(PREVIEW_CHAT, nodeId, { state: face });
|
|
201
233
|
}
|
|
234
|
+
// THE TEMPLATE ITSELF, wearing its base face. The server-sent address is the
|
|
235
|
+
// templates URI, so the instance resolves the template definition, not a component.
|
|
236
|
+
const ref = refOf(current);
|
|
237
|
+
store.apply({
|
|
238
|
+
type: "COMPONENT_INIT",
|
|
239
|
+
chatId: PREVIEW_CHAT,
|
|
240
|
+
nodeId: TEMPLATE_NODE,
|
|
241
|
+
component: { type: ref, componentUrl: `unoverse://templates/${ref}`, props: {} },
|
|
242
|
+
});
|
|
243
|
+
const base = current.defaultLayout ?? (current.layouts ? Object.keys(current.layouts)[0] : undefined);
|
|
244
|
+
if (base) store.mergeComponentState(PREVIEW_CHAT, TEMPLATE_NODE, { state: base });
|
|
202
245
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
203
246
|
}, [store, current]);
|
|
204
247
|
|
|
@@ -210,13 +253,25 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
210
253
|
useCallback(() => store.getStructureVersion(), [store]),
|
|
211
254
|
);
|
|
212
255
|
const shapeNames = current?.layouts ? Object.keys(current.layouts) : [];
|
|
256
|
+
// TWO WAYS A TEMPLATE REACHES A SCREEN (TEMPLATE_MODEL §5d). One HOLDS things: its state
|
|
257
|
+
// is entered by what arrives into it (a card writing itself into `page`), so the face is
|
|
258
|
+
// derived from the items it holds — a MOUNT. The other IS the thing: a whole-surface task
|
|
259
|
+
// with nothing browsed, whose own ✕ and chip write its own state. Which one is a fact of
|
|
260
|
+
// the template: does its base arrangement seed any items?
|
|
261
|
+
const holdsItems = useMemo(() => seeds.some((sd) => !sd.framed), [seeds]);
|
|
213
262
|
const activeState = useMemo(() => {
|
|
214
263
|
void storeVersion;
|
|
264
|
+
if (!holdsItems) {
|
|
265
|
+
const own = store.get(PREVIEW_CHAT, TEMPLATE_NODE) as Record<string, unknown> | undefined;
|
|
266
|
+
const face = own?.state ?? own?.view ?? own?.defaultState;
|
|
267
|
+
return typeof face === "string" && shapeNames.includes(face) ? face : (shapeNames[0] ?? "");
|
|
268
|
+
}
|
|
215
269
|
for (const name of shapeNames.slice(1)) {
|
|
216
270
|
const hit = store
|
|
217
271
|
.getResponses()
|
|
218
272
|
.some((r) => r.components.some((p) => {
|
|
219
273
|
const { chatId, nodeId } = store.split(p);
|
|
274
|
+
if (nodeId === TEMPLATE_NODE) return false;
|
|
220
275
|
const slice = store.get(chatId, nodeId) as Record<string, unknown> | undefined;
|
|
221
276
|
return (slice?.state ?? slice?.view ?? slice?.defaultState) === name;
|
|
222
277
|
}));
|
|
@@ -224,7 +279,16 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
224
279
|
}
|
|
225
280
|
return shapeNames[0] ?? "";
|
|
226
281
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
-
}, [storeVersion, store, current]);
|
|
282
|
+
}, [storeVersion, store, current, holdsItems]);
|
|
283
|
+
// A MOUNT'S FACE IS DERIVED, and the instance's root Switch draws by ITS state, so the
|
|
284
|
+
// derivation is mirrored onto the instance: a card tapping into `page` turns the page
|
|
285
|
+
// shape, exactly as the mount would. A self-faced template needs no mirror — its own
|
|
286
|
+
// state IS the derivation above.
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (!holdsItems || !activeState) return;
|
|
289
|
+
const own = store.get(PREVIEW_CHAT, TEMPLATE_NODE) as Record<string, unknown> | undefined;
|
|
290
|
+
if ((own?.state ?? own?.view ?? own?.defaultState) !== activeState) store.mergeComponentState(PREVIEW_CHAT, TEMPLATE_NODE, { state: activeState });
|
|
291
|
+
}, [store, holdsItems, activeState]);
|
|
228
292
|
|
|
229
293
|
// GROUPED BY CATEGORY, exactly as Components groups its rail — the shelf is no longer a
|
|
230
294
|
// flat list the moment small templates land on it (the `Blocks` shelf: pair, feature,
|
|
@@ -311,9 +375,23 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
311
375
|
color: metaOpen ? theme.color["text.primary"] : theme.color["text.secondary"],
|
|
312
376
|
background: metaOpen ? theme.color["surface.sunken"] : "transparent",
|
|
313
377
|
}}
|
|
314
|
-
title="Show the template's manifest
|
|
378
|
+
title="Show the template's manifest: its meta"
|
|
315
379
|
>
|
|
316
|
-
ⓘ
|
|
380
|
+
ⓘ Manifest <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
|
|
381
|
+
</button>
|
|
382
|
+
)}
|
|
383
|
+
{current && (
|
|
384
|
+
<button
|
|
385
|
+
onClick={copyForCanvas}
|
|
386
|
+
className="rounded border px-2 py-0.5 text-[11px] font-medium transition"
|
|
387
|
+
style={{
|
|
388
|
+
borderColor: theme.color["border.subtle"],
|
|
389
|
+
color: copiedForCanvas ? theme.color["text.primary"] : theme.color["text.secondary"],
|
|
390
|
+
background: copiedForCanvas ? theme.color["surface.sunken"] : "transparent",
|
|
391
|
+
}}
|
|
392
|
+
title="Copy this template as a canvas node — paste (Cmd+V) on any workflow canvas; wire an Agent to its port to fill it"
|
|
393
|
+
>
|
|
394
|
+
{copiedForCanvas ? "✓ Copied — paste on canvas" : "⧉ Copy for Canvas"}
|
|
317
395
|
</button>
|
|
318
396
|
)}
|
|
319
397
|
{shapeNames.length > 1 && (
|
|
@@ -337,10 +415,17 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
337
415
|
// detail shape surfaced with nothing in it. Slots with an
|
|
338
416
|
// authored `state:` are skipped, and the detail pill opens
|
|
339
417
|
// the first ITEM.
|
|
418
|
+
// A SELF-FACED TEMPLATE (a task: nothing browsed) is one instance,
|
|
419
|
+
// and the pill writes ITS state — the same write its own chip and
|
|
420
|
+
// ✕ make. A mount's pill re-faces the items it holds, below.
|
|
421
|
+
if (!holdsItems) {
|
|
422
|
+
store.mergeComponentState(PREVIEW_CHAT, TEMPLATE_NODE, { state: name });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
340
425
|
const items = store
|
|
341
426
|
.getResponses()
|
|
342
427
|
.flatMap((r) => r.components)
|
|
343
|
-
.filter((ptr) =>
|
|
428
|
+
.filter((ptr) => { const n = store.split(ptr).nodeId; return n !== TEMPLATE_NODE && !framedIds.has(n); });
|
|
344
429
|
for (const [j, ptr] of items.entries()) {
|
|
345
430
|
const { chatId, nodeId } = store.split(ptr);
|
|
346
431
|
const slice = store.get(chatId, nodeId) as Record<string, unknown> | undefined;
|
|
@@ -419,7 +504,7 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
419
504
|
|
|
420
505
|
<div className="flex-1 overflow-auto p-10">
|
|
421
506
|
<div className="mx-auto max-w-full transition-[width] duration-200" style={{ width: stageWidth ?? "100%" }}>
|
|
422
|
-
{uri && <
|
|
507
|
+
{uri && <UnoverseComponent key={uri} client={client} store={store} chatId={PREVIEW_CHAT} nodeId={TEMPLATE_NODE} theme={theme} />}
|
|
423
508
|
</div>
|
|
424
509
|
</div>
|
|
425
510
|
</section>
|