@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.
- package/bin/studio.mjs +2 -2
- package/local/catalog.ts +5 -1
- package/local/nodes.ts +28 -41
- package/local/plugin.ts +71 -13
- package/local/sample-identity/brand.yaml +24 -0
- package/local/sample-identity/organisation.yaml +14 -0
- package/local/sample-identity/purpose.yaml +14 -0
- package/local/sample-identity/story.yaml +18 -0
- package/local/scaffold.mjs +43 -23
- package/local/source.ts +3 -8
- package/package.json +1 -1
- package/screens/AgentsView.tsx +173 -0
- package/screens/ComponentsView.tsx +2 -2
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/IdentityView.tsx +3 -3
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +2 -0
- package/src/App.tsx +8 -3
- package/src/host.ts +1 -14
- package/vendor/base/src/agent-mcp/discovery.ts +76 -18
- package/vendor/base/src/agent-mcp/handoff.ts +11 -31
- package/vendor/base/src/agent-mcp/index.ts +5 -5
- package/vendor/base/src/agent-mcp/invoke.ts +37 -19
- package/vendor/base/src/agent-mcp/lean.ts +19 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
- package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
- package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
- package/vendor/base/src/agent-mcp/tools.ts +136 -7
- package/vendor/base/src/agent-mcp/types.ts +47 -10
- package/vendor/base/src/auth/auth.ts +35 -5
- package/vendor/base/src/definitions/isolated.ts +37 -0
- 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 +117 -0
- package/vendor/base/src/definitions/registry/catalog.ts +40 -24
- package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
- package/vendor/base/src/definitions/registry/homes.ts +69 -5
- package/vendor/base/src/definitions/registry/objects.ts +413 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
- package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
- package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
- package/vendor/base/src/definitions/theme.ts +1 -2
- package/vendor/base/src/items/collect.ts +29 -25
- package/vendor/base/src/items/publish.ts +30 -6
- package/vendor/base/src/lint/context.mjs +17 -0
- package/vendor/base/src/lint/design/file.mjs +199 -2
- package/vendor/base/src/lint/design/index.mjs +66 -73
- package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
- package/vendor/base/src/lint/design/walk.mjs +14 -4
- package/vendor/base/src/lint/index.mjs +15 -9
- package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
- package/vendor/base/src/lint/nodes/index.mjs +5 -0
- package/vendor/base/src/lint/nodes/node.mjs +157 -7
- package/vendor/base/src/lint/objects/index.d.mts +32 -0
- package/vendor/base/src/lint/objects/index.mjs +324 -0
- package/vendor/base/src/lint/objects/rules.d.mts +44 -0
- package/vendor/base/src/lint/objects/rules.mjs +446 -0
- package/vendor/base/src/lint/prompts/index.d.mts +34 -0
- package/vendor/base/src/lint/prompts/index.mjs +198 -0
- package/vendor/base/src/manifests/compose.ts +80 -9
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +9 -1
- package/vendor/base/src/manifests/executor/memory.ts +83 -3
- package/vendor/base/src/manifests/executor/session.ts +50 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
- package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
- package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
- package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
- package/vendor/base/src/manifests/runtime/context.ts +18 -6
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
- package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
- package/vendor/base/src/manifests/runtime/events.ts +39 -3
- package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
- package/vendor/base/src/manifests/runtime/index.ts +3 -2
- package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
- package/vendor/base/src/manifests/runtime/usage.ts +10 -0
- package/vendor/base/src/paths.ts +16 -8
- package/vendor/base/src/platform/audioManager.ts +32 -0
- package/vendor/base/src/platform/executionContext.ts +14 -0
- package/vendor/base/src/platform/liveInput.ts +52 -0
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/template/SafeExpression.ts +6 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
- package/vendor/sdk/appEngine.tsx +30 -11
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
- package/vendor/sdk/lib/app.tsx +70 -95
- package/vendor/sdk/lib/connection.tsx +47 -57
- package/vendor/sdk/lib/core/actions.ts +6 -0
- package/vendor/sdk/lib/core/apps.ts +66 -81
- package/vendor/sdk/lib/core/client.ts +5 -14
- package/vendor/sdk/lib/core/connection.ts +30 -0
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +90 -15
- package/vendor/sdk/lib/core/types.ts +9 -8
- package/vendor/sdk/lib/primitives.tsx +113 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
- package/vendor/sdk/lib/render.tsx +3 -3
- package/vendor/sdk/lib/voice.tsx +27 -13
- package/local/sample-identity/brand/brand.yaml +0 -88
- package/local/sample-identity/brand/manifest.yaml +0 -13
- package/local/sample-identity/organisation/manifest.yaml +0 -15
- package/local/sample-identity/organisation/organisation.yaml +0 -53
- package/local/sample-identity/purpose/manifest.yaml +0 -12
- package/local/sample-identity/purpose/purpose.yaml +0 -46
- package/local/sample-identity/story/manifest.yaml +0 -12
- package/local/sample-identity/story/story.yaml +0 -63
- package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
|
@@ -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
|
*/
|
package/screens/IdentityView.tsx
CHANGED
|
@@ -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
|
|
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/
|
|
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
|
|
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/
|
|
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,
|
|
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>
|
|
@@ -647,7 +647,11 @@ export function TemplatesView({
|
|
|
647
647
|
...restState,
|
|
648
648
|
...defaultsOf(c),
|
|
649
649
|
...(data ?? {}),
|
|
650
|
-
|
|
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
|
|
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 [
|
|
826
|
-
if (!
|
|
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
|
-
|
|
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
|
|
1026
|
+
title="Show the app's manifest: its meta, its binding, its shape"
|
|
1012
1027
|
>
|
|
1013
|
-
|
|
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
|