@unoverse-platform/studio 0.1.97 → 0.1.99

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 (120) hide show
  1. package/bin/studio.mjs +2 -2
  2. package/local/catalog.ts +5 -1
  3. package/local/nodes.ts +28 -41
  4. package/local/plugin.ts +71 -13
  5. package/local/sample-identity/brand.yaml +24 -0
  6. package/local/sample-identity/organisation.yaml +14 -0
  7. package/local/sample-identity/purpose.yaml +14 -0
  8. package/local/sample-identity/story.yaml +18 -0
  9. package/local/scaffold.mjs +43 -23
  10. package/local/source.ts +3 -8
  11. package/package.json +1 -1
  12. package/screens/AgentsView.tsx +173 -0
  13. package/screens/ComponentsView.tsx +2 -2
  14. package/screens/EmptyLibrary.tsx +1 -1
  15. package/screens/IdentityView.tsx +3 -3
  16. package/screens/PromptBlocksView.tsx +1 -1
  17. package/screens/TemplatesLibraryView.tsx +91 -6
  18. package/screens/TemplatesView.tsx +24 -9
  19. package/screens/index.ts +2 -0
  20. package/src/App.tsx +8 -3
  21. package/src/host.ts +1 -14
  22. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  23. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  24. package/vendor/base/src/agent-mcp/index.ts +5 -5
  25. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  26. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  27. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  28. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  29. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  30. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  31. package/vendor/base/src/agent-mcp/types.ts +47 -10
  32. package/vendor/base/src/auth/auth.ts +35 -5
  33. package/vendor/base/src/definitions/isolated.ts +37 -0
  34. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  35. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  36. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  37. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  38. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  39. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  40. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  41. package/vendor/base/src/definitions/load/load.ts +37 -28
  42. package/vendor/base/src/definitions/model.ts +8 -12
  43. package/vendor/base/src/definitions/registry/agents.ts +117 -0
  44. package/vendor/base/src/definitions/registry/catalog.ts +40 -24
  45. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  46. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  47. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  48. package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
  49. package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
  50. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  51. package/vendor/base/src/definitions/theme.ts +1 -2
  52. package/vendor/base/src/items/collect.ts +29 -25
  53. package/vendor/base/src/items/publish.ts +30 -6
  54. package/vendor/base/src/lint/context.mjs +17 -0
  55. package/vendor/base/src/lint/design/file.mjs +199 -2
  56. package/vendor/base/src/lint/design/index.mjs +66 -73
  57. package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
  58. package/vendor/base/src/lint/design/walk.mjs +14 -4
  59. package/vendor/base/src/lint/index.mjs +15 -9
  60. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  61. package/vendor/base/src/lint/nodes/index.mjs +5 -0
  62. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  63. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  64. package/vendor/base/src/lint/objects/index.mjs +324 -0
  65. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  66. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  67. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  68. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  69. package/vendor/base/src/manifests/compose.ts +80 -9
  70. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  71. package/vendor/base/src/manifests/executor/context.ts +9 -1
  72. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  73. package/vendor/base/src/manifests/executor/session.ts +50 -0
  74. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  75. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  79. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  80. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  81. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  82. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  83. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  84. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  85. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  86. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  87. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  88. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  89. package/vendor/base/src/paths.ts +16 -8
  90. package/vendor/base/src/platform/audioManager.ts +32 -0
  91. package/vendor/base/src/platform/executionContext.ts +14 -0
  92. package/vendor/base/src/platform/liveInput.ts +52 -0
  93. package/vendor/base/src/plugins/discovery.ts +1 -1
  94. package/vendor/base/src/template/SafeExpression.ts +6 -0
  95. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  96. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  97. package/vendor/sdk/appEngine.tsx +30 -11
  98. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  99. package/vendor/sdk/lib/app.tsx +70 -95
  100. package/vendor/sdk/lib/connection.tsx +47 -57
  101. package/vendor/sdk/lib/core/actions.ts +6 -0
  102. package/vendor/sdk/lib/core/apps.ts +66 -81
  103. package/vendor/sdk/lib/core/client.ts +5 -14
  104. package/vendor/sdk/lib/core/connection.ts +30 -0
  105. package/vendor/sdk/lib/core/index.ts +2 -2
  106. package/vendor/sdk/lib/core/store.ts +90 -15
  107. package/vendor/sdk/lib/core/types.ts +9 -8
  108. package/vendor/sdk/lib/primitives.tsx +113 -4
  109. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  110. package/vendor/sdk/lib/render.tsx +3 -3
  111. package/vendor/sdk/lib/voice.tsx +27 -13
  112. package/local/sample-identity/brand/brand.yaml +0 -88
  113. package/local/sample-identity/brand/manifest.yaml +0 -13
  114. package/local/sample-identity/organisation/manifest.yaml +0 -15
  115. package/local/sample-identity/organisation/organisation.yaml +0 -53
  116. package/local/sample-identity/purpose/manifest.yaml +0 -12
  117. package/local/sample-identity/purpose/purpose.yaml +0 -46
  118. package/local/sample-identity/story/manifest.yaml +0 -12
  119. package/local/sample-identity/story/story.yaml +0 -63
  120. package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
@@ -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
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Identity view — the org's four documents (organisation, brand, purpose, story) as the
3
- * pack holds them: design/<org>/identity/<doc>/. Left: a navigator, one group per org
3
+ * pack holds them: design/<org>/identity/<doc>.yaml. Left: a navigator, one group per org
4
4
  * (or the one org this Studio is bound to). Right: the selected document's manifest words,
5
5
  * then each prop with its brief and its default, the org's own words.
6
6
  *
@@ -9,7 +9,7 @@
9
9
  * master switch every other kind has: Available puts a document in the spatial catalogue,
10
10
  * and each canvas installs it from there. The Organisation's switch is the grounding: on,
11
11
  * every run as this org wakes with it; it never sits on the map
12
- * (docs/edgy/elements/organisation.md).
12
+ * (docs/unoverse/data/objects/elements/organisation.md).
13
13
  *
14
14
  * Shared by both Studios (packages/studio/screens); the host supplies fetch + toggles.
15
15
  */
@@ -146,7 +146,7 @@ export function IdentityView({ org = "all" }: { org?: string } = {}) {
146
146
  noun="identity documents"
147
147
  org={org}
148
148
  folder="identity"
149
- shape="identity/ holds four definitions: organisation, brand, purpose and story, each a manifest and a document whose props carry the brief and the org's own words."
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
150
  error={error}
151
151
  />
152
152
  </div>
@@ -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
  *
@@ -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>
@@ -647,7 +647,11 @@ export function TemplatesView({
647
647
  ...restState,
648
648
  ...defaultsOf(c),
649
649
  ...(data ?? {}),
650
- ...(view ? { view, defaultState: view } : { defaultState: "inline" }),
650
+ // ALL THREE SPELLINGS, as the activation below writes them: `state` is the ruled
651
+ // public-axis word (2026-08-29) and what a synthesized `Switch on: state` reads;
652
+ // with only the legacy two, the ladder placed a deal in the grid while the deal
653
+ // drew its FIRST face (observed 2026-09-11: list tiles inside a grid).
654
+ ...(view ? { state: view, view, defaultState: view } : { defaultState: "inline" }),
651
655
  },
652
656
  },
653
657
  });
@@ -815,19 +819,30 @@ export function TemplatesView({
815
819
  // a single-key object naming the component and carrying its own view + data
816
820
  // (UNOVERSE_INTERFACE_MODEL §3). Envelope instances arrive already in their view.
817
821
  const toSpec = (item: string | Record<string, Record<string, unknown>>): SeedSpec | null => {
822
+ // THE HASH NAMES THE STATE IT OPENS IN (STATE_MODEL rule 3; owner 2026-09-11: a
823
+ // preview is written `deal#grid`, the same address grammar as everywhere else).
824
+ // Split ONCE, for both forms: a mount state wraps each name as `{ "deal#grid": {} }`
825
+ // before it gets here, so the split lives beside the lookup, not on one branch.
826
+ const split = (ref: string): [string, string | undefined] => {
827
+ const [name, hash] = ref.split("#");
828
+ return [name, hash || undefined];
829
+ };
818
830
  if (typeof item === "string") {
819
- const def = byName(item);
831
+ const [name, hash] = split(item);
832
+ const def = byName(name);
820
833
  // In the FLAT (container-model) list every entry is an envelope: a bare name
821
834
  // wakes in its own arrival face. Only the keyed legacy form seeds inline for
822
- // the state chip to move.
823
- return def ? { def, ...(flatPreview ? { envelope: true } : {}) } : null;
835
+ // the state chip to move. A hash seeds the instance's own view, as an envelope.
836
+ return def ? { def, ...(hash ? { envelope: true, view: hash } : flatPreview ? { envelope: true } : {}) } : null;
824
837
  }
825
- const [name, body] = Object.entries(item ?? {})[0] ?? [];
826
- if (!name) return null;
838
+ const [rawName, body] = Object.entries(item ?? {})[0] ?? [];
839
+ if (!rawName) return null;
840
+ const [name, hash] = split(rawName);
827
841
  const def = byName(name);
828
842
  if (!def) return null;
829
843
  const { view, ...data } = (body ?? {}) as Record<string, unknown>;
830
- return { def, envelope: true, ...(typeof view === "string" && view ? { view } : {}), ...(Object.keys(data).length ? { data } : {}) };
844
+ const face = typeof view === "string" && view ? view : hash;
845
+ return { def, envelope: true, ...(face ? { view: face } : {}), ...(Object.keys(data).length ? { data } : {}) };
831
846
  };
832
847
  const set = entry
833
848
  ? entry.map(toSpec).filter((s): s is SeedSpec => Boolean(s))
@@ -1008,9 +1023,9 @@ export function TemplatesView({
1008
1023
  onClick={() => setMetaOpen((o) => !o)}
1009
1024
  className="flex items-center gap-1 rounded border px-2 py-0.5 font-medium"
1010
1025
  style={{ borderColor: theme.color["border.subtle"], color: metaOpen ? theme.color["text.primary"] : theme.color["text.secondary"], background: metaOpen ? theme.color["surface.sunken"] : "transparent" }}
1011
- title="Show the MCP app manifest"
1026
+ title="Show the app's manifest: its meta, its binding, its shape"
1012
1027
  >
1013
- {selectedDef?.app ? "ⓘ MCP app" : "ⓘ Details"} <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
1028
+ ⓘ Manifest <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
1014
1029
  </button>
1015
1030
  )}
1016
1031
 
package/screens/index.ts CHANGED
@@ -24,6 +24,8 @@ export { TemplatesView } from "./TemplatesView";
24
24
  export { TemplatesLibraryView } from "./TemplatesLibraryView";
25
25
  export { DesignSystemView } from "./DesignSystemView";
26
26
  export { SkillsView } from "./SkillsView";
27
+ // Agents: the one file authored for the map (docs.unoverse.ai/design/tasks), one lane in both Studios.
28
+ export { AgentsView } from "./AgentsView";
27
29
  export { IdentityView } from "./IdentityView";
28
30
  export { PromptBlocksView } from "./PromptBlocksView";
29
31
  export { IdentityHeader } from "./IdentityHeader";
package/src/App.tsx CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  TemplatesView,
16
16
  TemplatesLibraryView,
17
17
  DesignSystemView,
18
- SkillsView,
18
+ SkillsView, AgentsView,
19
19
  IdentityView,
20
20
  PromptBlocksView,
21
21
  } from "@unoverse/studio";
@@ -32,22 +32,25 @@ import {
32
32
  LayoutTemplate,
33
33
  Palette,
34
34
  Sparkles,
35
+ Bot,
35
36
  SquareDashed,
36
37
  TextQuote,
37
38
  Landmark,
38
39
  type LucideIcon,
39
40
  } from "lucide-react";
40
41
 
41
- type View = "apps" | "templates" | "components" | "atoms" | "tokens" | "identity" | "skills" | "prompts" | "nodes";
42
+ type View = "apps" | "agents" | "templates" | "components" | "atoms" | "tokens" | "identity" | "skills" | "prompts" | "nodes";
42
43
 
43
44
  const NAV: { key: View; label: string; group: string; Icon: LucideIcon }[] = [
45
+ // Owner's order (2026-09-06): apps, agents, skills, templates, then the parts.
44
46
  { key: "apps", label: "Apps", group: "Build", Icon: LayoutDashboard },
47
+ { key: "agents", label: "Agents", group: "Build", Icon: Bot },
48
+ { key: "skills", label: "Skills", group: "Build", Icon: Sparkles },
45
49
  { key: "templates", label: "Templates", group: "Build", Icon: LayoutTemplate },
46
50
  { key: "components", label: "Components", group: "Build", Icon: SquareDashed },
47
51
  { key: "atoms", label: "Atoms", group: "Build", Icon: Atom },
48
52
  { key: "tokens", label: "Styles", group: "Build", Icon: Palette },
49
53
  { key: "identity", label: "Identity", group: "AI", Icon: Landmark },
50
- { key: "skills", label: "Skills", group: "AI", Icon: Sparkles },
51
54
  { key: "prompts", label: "Prompt Blocks", group: "AI", Icon: TextQuote },
52
55
  { key: "nodes", label: "Nodes", group: "Code", Icon: Boxes },
53
56
  ];
@@ -196,6 +199,8 @@ function Shell() {
196
199
  // Standalone Studio authors offline: open apps in MOCK (the defaultLive=true
197
200
  // resting mode is Canvas's, where the universe is right there).
198
201
  <TemplatesView theme={theme} themeName={variant} org={org} defaultLive={false} />
202
+ ) : view === "agents" ? (
203
+ <AgentsView org={org} />
199
204
  ) : view === "templates" ? (
200
205
  <TemplatesLibraryView theme={theme} org={org} />
201
206
  ) : view === "components" ? (
package/src/host.ts CHANGED
@@ -129,19 +129,6 @@ class LocalUnoverseClient {
129
129
  return def;
130
130
  }
131
131
 
132
- /**
133
- * The SYNCHRONOUS cached-definition peek (see the SDK client). Studio's `readDefinition`
134
- * fetches every time and keeps no cache — deliberately, so an edit on disk shows up on
135
- * the next read — so there is nothing to peek and the honest answer is `null`.
136
- *
137
- * `null` means "no opinion", never "no": the one caller (the latch derivation on send)
138
- * treats it as "no latch", which is right here. Studio previews a definition; it has no
139
- * live conversation to address a component from. Present so the structural contract holds
140
- * — the SDK takes this client by shape, so a missing method is a crash, not a type error.
141
- */
142
- peekDefinition(_uri: string): UnoverseDefinition | null {
143
- return null;
144
- }
145
132
 
146
133
  async readTheme(name = "light"): Promise<ResolvedTheme> {
147
134
  const r = await fetch(`/local/theme?ref=${encodeURIComponent(name)}`);
@@ -187,7 +174,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, React.Dispat
187
174
  return [value, setValue];
188
175
  }
189
176
 
190
- export type RegistryKind = "node" | "skill" | "app" | "prompt" | "component" | "template" | "identity";
177
+ export type RegistryKind = "node" | "skill" | "app" | "prompt" | "component" | "template" | "identity" | "agent";
191
178
 
192
179
  /**
193
180
  * Registry toggles decide what a RUNNING universe exposes; they persist to