@unoverse-platform/studio 0.1.7 → 0.1.9
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 +62 -7
- package/package.json +1 -1
- package/screens/ComponentsView.tsx +18 -6
- package/screens/SkillsView.tsx +6 -2
- package/screens/TemplatesView.tsx +7 -6
package/bin/studio.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { createServer } from "vite";
|
|
20
20
|
import { dirname, resolve, join } from "path";
|
|
21
21
|
import { fileURLToPath } from "url";
|
|
22
|
-
import { existsSync } from "fs";
|
|
22
|
+
import { existsSync, readdirSync } from "fs";
|
|
23
23
|
import { createInterface } from "readline/promises";
|
|
24
24
|
import { createFirstProject, checkProjectName } from "../local/scaffold.mjs";
|
|
25
25
|
|
|
@@ -47,6 +47,33 @@ function findProjectRootFromCwd() {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A project ONE LEVEL DOWN.
|
|
52
|
+
*
|
|
53
|
+
* The walk above only goes UP, which was right while a project WAS the folder you stood
|
|
54
|
+
* in. Now that the first run creates ./<name>/, a second run from the parent would find no
|
|
55
|
+
* rx/ above it and offer to scaffold a SECOND project beside the first one. So look down
|
|
56
|
+
* as well.
|
|
57
|
+
*
|
|
58
|
+
* ONE level, and only when there is exactly one candidate. A deeper scan would adopt some
|
|
59
|
+
* unrelated project three folders away, and picking between several would be a coin toss;
|
|
60
|
+
* reading the wrong project's disk is silent, because both folders are perfectly valid.
|
|
61
|
+
*/
|
|
62
|
+
function findProjectBelowCwd() {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
const found = readdirSync(cwd, { withFileTypes: true })
|
|
65
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
|
|
66
|
+
.filter((e) => existsSync(join(cwd, e.name, "rx")))
|
|
67
|
+
.map((e) => e.name);
|
|
68
|
+
|
|
69
|
+
if (found.length === 1) return join(cwd, found[0]);
|
|
70
|
+
if (found.length > 1) {
|
|
71
|
+
console.log(`\n Several projects here: ${found.join(", ")}\n\n cd into the one you want.\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
async function scaffoldIfEmpty() {
|
|
51
78
|
const cwd = process.cwd();
|
|
52
79
|
// The same walk findProjectRoot does. A project ABOVE us is a project; do not offer to
|
|
@@ -58,24 +85,52 @@ async function scaffoldIfEmpty() {
|
|
|
58
85
|
dir = parent;
|
|
59
86
|
}
|
|
60
87
|
|
|
88
|
+
// A project made by an earlier run, in the folder that run created.
|
|
89
|
+
const below = findProjectBelowCwd();
|
|
90
|
+
if (below) {
|
|
91
|
+
process.chdir(below);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
console.log(`\n No project here yet.\n\n ${cwd}\n`);
|
|
62
96
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
63
97
|
|
|
64
98
|
// Ask until the name is usable. It becomes a folder AND an item's `org` column, so a
|
|
65
99
|
// rejection here is cheaper than a rename after something has been published.
|
|
66
|
-
let name;
|
|
100
|
+
let name, home;
|
|
67
101
|
for (;;) {
|
|
68
102
|
name = (await rl.question(" Name your project [my-project]: ")).trim() || "my-project";
|
|
69
103
|
const bad = checkProjectName(name);
|
|
70
|
-
if (
|
|
71
|
-
|
|
104
|
+
if (bad) {
|
|
105
|
+
console.log(` ${bad}\n`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* THE NAME OWNS A FOLDER. Studio used to scaffold into the cwd, which made whatever
|
|
111
|
+
* directory you happened to be standing in your repository: run it once in a scratch
|
|
112
|
+
* folder and that scratch folder now holds a real project, with the name you typed
|
|
113
|
+
* used only for rx/<name>/. Asking for a name and then not putting it anywhere you
|
|
114
|
+
* can see reads as the tool ignoring you.
|
|
115
|
+
*/
|
|
116
|
+
home = join(cwd, name);
|
|
117
|
+
if (existsSync(home) && readdirSync(home).some((f) => !f.startsWith("."))) {
|
|
118
|
+
console.log(` ./${name} already has something in it\n`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
72
122
|
}
|
|
73
123
|
rl.close();
|
|
74
124
|
|
|
75
125
|
// The same call the UI's New project makes, so the first project and the fifth are
|
|
76
|
-
// identical (local/scaffold.mjs).
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
// identical (local/scaffold.mjs). rx/<name>/ stays nested: every subfolder of rx/ IS a
|
|
127
|
+
// project (base collect.ts listProjects) and its folder name is the `org` that
|
|
128
|
+
// publishes, so a flat rx/ could hold exactly one project and would read `components`
|
|
129
|
+
// and `templates` as orgs.
|
|
130
|
+
createFirstProject(home, name);
|
|
131
|
+
// Studio reads from the cwd, and the project is no longer the folder we launched in.
|
|
132
|
+
process.chdir(home);
|
|
133
|
+
console.log(`\n Created ${name}/ with rx/${name}/, prompts/ and nodes/\n`);
|
|
79
134
|
}
|
|
80
135
|
|
|
81
136
|
await scaffoldIfEmpty();
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ import { UnoverseComponent, type ResolvedTheme } from "@unoverse/sdk";
|
|
|
9
9
|
import { client, authedFetch, hasSourceAccess } from "studio-host";
|
|
10
10
|
import { SourceView } from "./SourceView";
|
|
11
11
|
import { stateActivation } from "./states";
|
|
12
|
-
import { usePersistentState } from "studio-host";
|
|
12
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
13
13
|
|
|
14
14
|
const CHAT_ID = "demo";
|
|
15
15
|
|
|
@@ -58,6 +58,7 @@ const VIEWPORTS: { id: string; label: string; w: number | null }[] = [
|
|
|
58
58
|
export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; org?: string }) {
|
|
59
59
|
const store = useMemo(() => new ComponentStore(), []);
|
|
60
60
|
const [components, setComponents] = useState<ComponentDef[]>([]);
|
|
61
|
+
const available = useRegistryToggles("component");
|
|
61
62
|
const [selected, setSelected] = usePersistentState<string | null>("wb:components:selected", null);
|
|
62
63
|
const [values, setValues] = useState<Record<string, unknown>>({}); // typed values → store/render
|
|
63
64
|
const [text, setText] = useState<Record<string, string>>({}); // raw editor strings (decoupled from the typed value)
|
|
@@ -500,11 +501,22 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
500
501
|
{/* Spatial toggle — ONLY for spatial-capable components (manifest.json present).
|
|
501
502
|
Same two-level model as templates/skills: this is Level 1 (workbench
|
|
502
503
|
eligibility); the Content Engine's per-workflow toggle is Level 2. */}
|
|
503
|
-
{/*
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
504
|
+
{/* AVAILABLE — the MASTER switch, and the first of two.
|
|
505
|
+
Studio available: is this offered to workflows at all? (here)
|
|
506
|
+
A canvas installed: is it on THIS workflow's map? (Content Library)
|
|
507
|
+
Studio decides the menu; each canvas orders from it. Both exist because
|
|
508
|
+
spatial is per workflow and an org can have hundreds of them carrying
|
|
509
|
+
different components.
|
|
510
|
+
It is NOT called "Spatial": two controls both called that, one of which the
|
|
511
|
+
catalogue ignored, is what made this switch look authoritative and do
|
|
512
|
+
nothing. This one is honoured — off means no canvas can install it. */}
|
|
513
|
+
{current?.spatial && (
|
|
514
|
+
<ToggleSwitch
|
|
515
|
+
on={available.isEnabled(current.name)}
|
|
516
|
+
onClick={() => available.toggle(current.name)}
|
|
517
|
+
label="Available"
|
|
518
|
+
/>
|
|
519
|
+
)}
|
|
508
520
|
{current?.spatial && (
|
|
509
521
|
<button
|
|
510
522
|
onClick={() => setMetaOpen((o) => !o)}
|
package/screens/SkillsView.tsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { useEffect, useState } from "react";
|
|
13
13
|
import { fetchUnoverse } from "studio-host";
|
|
14
|
-
import { usePersistentState } from "studio-host";
|
|
14
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
15
15
|
import { IdentityHeader } from "./IdentityHeader";
|
|
16
16
|
|
|
17
17
|
interface SkillSummary {
|
|
@@ -33,6 +33,7 @@ interface SkillDetail extends SkillSummary {
|
|
|
33
33
|
|
|
34
34
|
export function SkillsView() {
|
|
35
35
|
const [skills, setSkills] = useState<SkillSummary[]>([]);
|
|
36
|
+
const available = useRegistryToggles("skill");
|
|
36
37
|
const [selected, setSelected] = usePersistentState<string | null>("wb:skills:selected", null);
|
|
37
38
|
const [detail, setDetail] = useState<SkillDetail | null>(null);
|
|
38
39
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
|
@@ -122,14 +123,17 @@ export function SkillsView() {
|
|
|
122
123
|
<div className="flex h-full items-center justify-center text-sm text-gray-500">Select a skill</div>
|
|
123
124
|
) : (
|
|
124
125
|
<div className="mx-auto max-w-3xl p-8">
|
|
126
|
+
{/* AVAILABLE — the master switch. Which workflows actually carry this skill is
|
|
127
|
+
installed per workflow, in Canvas's Content Library. */}
|
|
125
128
|
<IdentityHeader
|
|
129
|
+
right={<ToggleSwitch on={available.isEnabled(selected)} onClick={() => available.toggle(selected)} label="Available" />}
|
|
126
130
|
name={detail?.name ?? selected}
|
|
127
131
|
category={detail?.category}
|
|
128
132
|
version={detail?.version}
|
|
129
133
|
description={detail?.description}
|
|
130
134
|
whenToUse={detail?.whenToUse}
|
|
131
135
|
/>
|
|
132
|
-
<p className="mb-3 text-[11px] text-gray-500">
|
|
136
|
+
<p className="mb-3 text-[11px] text-gray-500">Available means workflows may install this skill. Which ones actually have it is chosen per workflow, in Canvas’s Content Library.</p>
|
|
133
137
|
|
|
134
138
|
{detail?.triggers && detail.triggers.length > 0 && (
|
|
135
139
|
<div className="mb-5 flex flex-wrap gap-1.5">
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
11
11
|
import { useAuth } from "react-oidc-context";
|
|
12
|
-
import { usePersistentState } from "studio-host";
|
|
12
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
13
13
|
import { ComponentStore } from "@unoverse/sdk";
|
|
14
14
|
import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resolveActiveLayout, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
|
|
15
15
|
import { NativeAppHost } from "./NativeAppHost";
|
|
@@ -131,6 +131,7 @@ export function TemplatesView({
|
|
|
131
131
|
// MOCK (offline) vs LIVE (real data plane). A fresh store per mode keeps the two paths
|
|
132
132
|
// from bleeding into each other.
|
|
133
133
|
const [live, setLive] = useState(defaultLive);
|
|
134
|
+
const available = useRegistryToggles("template");
|
|
134
135
|
const [templates, setTemplates] = useState<Def[]>([]);
|
|
135
136
|
const [components, setComponents] = useState<Def[]>([]);
|
|
136
137
|
const [selected, setSelected] = usePersistentState<string | null>("wb:templates:selected", null);
|
|
@@ -635,11 +636,11 @@ export function TemplatesView({
|
|
|
635
636
|
})}
|
|
636
637
|
</div>
|
|
637
638
|
)}
|
|
638
|
-
{/*
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
639
|
+
{/* AVAILABLE — the master switch (see ComponentsView for the two-level model).
|
|
640
|
+
Studio offers; each canvas installs from what is offered. */}
|
|
641
|
+
{selected && (
|
|
642
|
+
<ToggleSwitch on={available.isEnabled(selected)} onClick={() => available.toggle(selected)} label="Available" />
|
|
643
|
+
)}
|
|
643
644
|
{selected && (
|
|
644
645
|
<button
|
|
645
646
|
onClick={() => setMetaOpen((o) => !o)}
|