@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.
Files changed (118) hide show
  1. package/bin/studio.mjs +3 -3
  2. package/local/authoring.ts +1 -1
  3. package/local/catalog.ts +22 -2
  4. package/local/keys.ts +1 -1
  5. package/local/nodes.ts +29 -42
  6. package/local/plugin.ts +75 -13
  7. package/local/sample-identity/brand.yaml +24 -0
  8. package/local/sample-identity/organisation.yaml +14 -0
  9. package/local/sample-identity/purpose.yaml +14 -0
  10. package/local/sample-identity/story.yaml +18 -0
  11. package/local/scaffold.mjs +51 -22
  12. package/local/source.ts +4 -9
  13. package/package.json +1 -1
  14. package/screens/AgentsView.tsx +173 -0
  15. package/screens/ComponentsView.tsx +2 -2
  16. package/screens/EmptyLibrary.tsx +1 -1
  17. package/screens/IdentityHeader.tsx +1 -1
  18. package/screens/IdentityView.tsx +206 -0
  19. package/screens/PromptBlocksView.tsx +1 -1
  20. package/screens/SourceView.tsx +1 -1
  21. package/screens/TemplatesLibraryView.tsx +91 -6
  22. package/screens/TemplatesView.tsx +24 -9
  23. package/screens/index.ts +5 -2
  24. package/src/App.tsx +14 -4
  25. package/src/NodeKeys.tsx +1 -1
  26. package/src/host.ts +3 -16
  27. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  28. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  29. package/vendor/base/src/agent-mcp/index.ts +5 -5
  30. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  31. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  32. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  33. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  34. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  35. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  36. package/vendor/base/src/agent-mcp/types.ts +47 -10
  37. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  38. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  39. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  40. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  41. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  42. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  43. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  44. package/vendor/base/src/definitions/load/load.ts +37 -28
  45. package/vendor/base/src/definitions/model.ts +8 -12
  46. package/vendor/base/src/definitions/registry/agents.ts +111 -0
  47. package/vendor/base/src/definitions/registry/catalog.ts +28 -12
  48. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  49. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  50. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  51. package/vendor/base/src/definitions/registry/orgDocuments.ts +107 -0
  52. package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
  53. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  54. package/vendor/base/src/definitions/theme.ts +1 -2
  55. package/vendor/base/src/items/collect.ts +33 -25
  56. package/vendor/base/src/items/publish.ts +31 -7
  57. package/vendor/base/src/lint/context.mjs +17 -0
  58. package/vendor/base/src/lint/design/file.mjs +199 -2
  59. package/vendor/base/src/lint/design/index.mjs +66 -73
  60. package/vendor/base/src/lint/design/vocabulary.mjs +6 -3
  61. package/vendor/base/src/lint/design/walk.mjs +14 -4
  62. package/vendor/base/src/lint/index.mjs +15 -9
  63. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  64. package/vendor/base/src/lint/nodes/index.mjs +6 -1
  65. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  66. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  67. package/vendor/base/src/lint/objects/index.mjs +324 -0
  68. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  69. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  70. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  71. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  72. package/vendor/base/src/manifests/compose.ts +81 -10
  73. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  74. package/vendor/base/src/manifests/executor/context.ts +12 -1
  75. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  76. package/vendor/base/src/manifests/executor/session.ts +50 -0
  77. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  78. package/vendor/base/src/manifests/index.ts +1 -1
  79. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  80. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  81. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  82. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  83. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  86. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  87. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  88. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  89. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  90. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  91. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  92. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  93. package/vendor/base/src/manifests/source.ts +1 -1
  94. package/vendor/base/src/paths.ts +16 -8
  95. package/vendor/base/src/platform/audioManager.ts +32 -0
  96. package/vendor/base/src/platform/executionContext.ts +14 -0
  97. package/vendor/base/src/platform/liveInput.ts +52 -0
  98. package/vendor/base/src/plugins/README.md +1 -1
  99. package/vendor/base/src/plugins/discovery.ts +1 -1
  100. package/vendor/base/src/template/SafeExpression.ts +6 -0
  101. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  102. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  103. package/vendor/sdk/appEngine.tsx +31 -12
  104. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  105. package/vendor/sdk/lib/app.tsx +70 -95
  106. package/vendor/sdk/lib/connection.tsx +47 -57
  107. package/vendor/sdk/lib/core/actions.ts +6 -0
  108. package/vendor/sdk/lib/core/apps.ts +66 -81
  109. package/vendor/sdk/lib/core/client.ts +5 -14
  110. package/vendor/sdk/lib/core/connection.ts +30 -0
  111. package/vendor/sdk/lib/core/index.ts +2 -2
  112. package/vendor/sdk/lib/core/store.ts +90 -15
  113. package/vendor/sdk/lib/core/types.ts +9 -8
  114. package/vendor/sdk/lib/primitives.tsx +113 -4
  115. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  116. package/vendor/sdk/lib/render.tsx +3 -3
  117. package/vendor/sdk/lib/voice.tsx +27 -13
  118. 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
- * rx/<project>/** every project listOrgs() reports, via projectDir()
18
- * prompts/** skills and prompt blocks
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -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 (spatial discovery meta)"
616
+ title="Show the component's manifest: its meta and its arrival"
617
617
  >
618
- ⓘ Meta <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
618
+ ⓘ Manifest <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
619
619
  </button>
620
620
  )}
621
621
 
@@ -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/prompts/blocks/`, which is the UNIVERSAL tier. Studio scoped to an org
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-r py-1.5 pl-3" style={{ borderLeft: `3px solid ${accent}`, background: "#f3f4f6" }}>
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/prompts/blocks) served by the Unoverse server's /prompt-blocks
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
  *
@@ -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, StreamedUnoverseApp, type ResolvedTheme } from "@unoverse/sdk";
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 (spatial discovery meta)"
378
+ title="Show the template's manifest: its meta"
315
379
  >
316
- ⓘ Meta <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
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) => !framedIds.has(store.split(ptr).nodeId));
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 && <StreamedUnoverseApp key={uri} client={client} store={store} uri={uri} theme={theme} />}
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>