@vgai/editor 0.4.0 → 0.4.1
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/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
- package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
- package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
- package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
- package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
- package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
- package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
- package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
- package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
- package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
- package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
- package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
- package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
- package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
- package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
- package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
- package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
- package/dist/assets/index-DiUacMXR.css +1 -0
- package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
- package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
- package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
- package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
- package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +27125 -1075
- package/package.json +2 -2
- package/server/asset-history-snapshots.ts +189 -0
- package/server/asset-library-routes.ts +404 -53
- package/server/catalog-import-recipe.ts +97 -0
- package/server/catalog-reimport.ts +141 -0
- package/server/cloud-asset-catalog.ts +71 -5
- package/server/dev.ts +1 -1
- package/server/editor-server.ts +106 -51
- package/server/editor-sse.ts +5 -0
- package/server/generative-execution-context.ts +45 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/packaged.ts +1 -1
- package/server/project-hmr-files.ts +3 -5
- package/server/project-output-writer.ts +440 -36
- package/server/project-tools.ts +529 -0
- package/server/server-utils.ts +14 -11
- package/src/EditorContext.tsx +4 -4
- package/src/action-registry.ts +12 -0
- package/src/asset-editor-context.tsx +9 -0
- package/src/asset-events.ts +3 -15
- package/src/asset-selection.ts +3 -0
- package/src/asset-workflow/asset-capabilities.ts +40 -0
- package/src/asset-workflow/asset-import-jobs.ts +108 -0
- package/src/asset-workflow/asset-library-query.ts +8 -1
- package/src/asset-workflow/asset-workflow-quality.ts +2 -0
- package/src/asset-workflow/import-contract.ts +2 -0
- package/src/asset-workflow/model-inspection.ts +92 -1
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +1 -0
- package/src/asset-workflow/project-asset-health.ts +3 -6
- package/src/asset-workflow/project-asset-operations.ts +119 -4
- package/src/command-listener.ts +37 -9
- package/src/components/ApplicationMenus.tsx +13 -15
- package/src/components/AssetBrowser.tsx +301 -26
- package/src/components/AssetImportDetails.tsx +207 -92
- package/src/components/AssetImportJobStatus.tsx +55 -0
- package/src/components/CenterDocuments.tsx +28 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DefaultEditorLayout.tsx +4 -0
- package/src/components/InspectorToolSection.tsx +12 -10
- package/src/components/OnlineAssetBrowser.tsx +197 -11
- package/src/components/ToolHost.tsx +8 -8
- package/src/components/ViewportOverlay.tsx +13 -26
- package/src/components/ViewportPanel.tsx +19 -14
- package/src/components/ViewportShadingMenu.tsx +103 -0
- package/src/components/WorkspaceDock.tsx +103 -17
- package/src/components/asset-documents.tsx +38 -11
- package/src/components/asset-editor-persistence.ts +300 -5
- package/src/components/asset-selection-section.tsx +83 -9
- package/src/components/asset-viewers/AudioViewer.tsx +4 -7
- package/src/components/asset-viewers/ModelViewer.tsx +168 -5
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
- package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
- package/src/components/asset-workflow.css +30 -3
- package/src/components/primitives/editor-icons.ts +1 -0
- package/src/components/project-tool-documents.tsx +298 -0
- package/src/components/status-contributions.tsx +38 -0
- package/src/components/tool-documents.tsx +20 -17
- package/src/components/tool-schema-form.tsx +258 -0
- package/src/components/workspace-dock.css +16 -0
- package/src/editor-api.ts +159 -22
- package/src/editor-store.ts +6 -5
- package/src/editor-viewport.ts +12 -7
- package/src/game-viewport-state.ts +28 -0
- package/src/model-thumbnail.ts +97 -9
- package/src/play-mode.ts +30 -3
- package/src/project-tool-discovery.ts +5 -5
- package/src/{project-operations.ts → project-tools.ts} +39 -26
- package/src/scene-sync.ts +0 -67
- package/src/storage/handle-storage.ts +5 -1
- package/src/storage/http-storage.ts +7 -2
- package/src/storage/mem-storage.ts +7 -1
- package/src/storage/types.ts +2 -0
- package/src/theme-css.ts +17 -7
- package/src/tool-loader.ts +152 -241
- package/src/viewport-shading-boundary.ts +15 -0
- package/src/widgets/index.ts +2 -1
- package/src/workspace-dock-controller.ts +8 -0
- package/src/workspace-document-registry.ts +4 -4
- package/src/workspace-state-persistence.ts +36 -10
- package/template/.claude/skills/editor/SKILL.md +13 -3
- package/template/AGENTS.md +100 -83
- package/template/package.json +16 -0
- package/template/public/scenes/default.vscn.json +1 -2
- package/template/scripts/playtest.ts +7 -37
- package/template/server/rooms/game-room.ts +2 -2
- package/template/src/data/README.md +3 -2
- package/template/src/data/tuning.data.json +1 -3
- package/template/src/data/tuning.schema.json +0 -12
- package/template/src/data/tuning.schema.ts +2 -9
- package/template/src/data/tuning.ts +1 -1
- package/template/src/scripts/registry.ts +0 -3
- package/template/src/tools/README.md +151 -16
- package/template/src/tools/autoplay.tool.ts +31 -0
- package/template/src/tools/autoplay.ts +15 -0
- package/template/src/tools/tuning.document.tsx +107 -0
- package/template/src/tools/tuning.tool.ts +24 -0
- package/template/src/tools/tuning.ts +9 -0
- package/template/src/ui/Button.stories.tsx +1 -1
- package/template/tests/acceptance/example.spec.ts +4 -59
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bAGhEnnO.css +0 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
- package/dist/project-scripts.bundle.js +0 -3917
- package/server/project-operations.ts +0 -343
- package/src/components/project-operation-documents.tsx +0 -226
- package/template/src/operations/README.md +0 -88
- package/template/src/scripts/components/data-spinner.ts +0 -86
- package/template/src/scripts/components/spin-lap-math.ts +0 -27
- package/template/src/tools/example.tool.tsx +0 -97
- package/template/src/tools/spin.tool.tsx +0 -101
- package/template/tests/logic/example.test.ts +0 -31
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registered project-tool discovery and execution.
|
|
3
|
+
*
|
|
4
|
+
* Project modules stay ordinary TS/JS. This server-side boundary is the only
|
|
5
|
+
* loader: it reads explicit `package.json#vgai.tools` registrations, asks the
|
|
6
|
+
* owning Vite server to load each callable module in Node, validates the
|
|
7
|
+
* exported ToolDefinition, and dispatches through the existing internal SDK
|
|
8
|
+
* registry. Optional React contributions are separate browser modules and are
|
|
9
|
+
* never imported merely because a callable tool was listed or executed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import type { OperationDefinition, OperationOutcome } from '../../vgai-sdk/src/registry';
|
|
17
|
+
import { OperationRegistry } from '../../vgai-sdk/src/registry';
|
|
18
|
+
import { runWithGenerativeExecutionCapture } from './generative-execution-context';
|
|
19
|
+
import { createProjectOutputWriter } from './project-output-writer';
|
|
20
|
+
|
|
21
|
+
export type ProjectModuleLoader = (absoluteModuleId: string) => Promise<unknown>;
|
|
22
|
+
|
|
23
|
+
export interface ProjectToolErrorSummary {
|
|
24
|
+
code: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
dataSchema?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProjectToolCatalogEntry {
|
|
30
|
+
name: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
description: string;
|
|
33
|
+
sourcePath: string;
|
|
34
|
+
inputSchema: unknown;
|
|
35
|
+
resultSchema: unknown;
|
|
36
|
+
errors: ProjectToolErrorSummary[];
|
|
37
|
+
requires: Record<string, boolean | undefined>;
|
|
38
|
+
host: 'node' | 'editor-browser' | 'runtime-page';
|
|
39
|
+
mutates: boolean;
|
|
40
|
+
supportsDryRun: boolean;
|
|
41
|
+
longRunning: boolean;
|
|
42
|
+
permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
|
|
43
|
+
contributions: ProjectToolContribution[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ToolContributionPoint =
|
|
47
|
+
| 'workspace.document'
|
|
48
|
+
| 'selection.inspector'
|
|
49
|
+
| 'workspace.utility';
|
|
50
|
+
|
|
51
|
+
export interface ProjectToolContribution {
|
|
52
|
+
id: string;
|
|
53
|
+
point: ToolContributionPoint;
|
|
54
|
+
title: string;
|
|
55
|
+
entryPath: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ProjectToolLoadError {
|
|
59
|
+
sourcePath: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ProjectToolCatalog {
|
|
64
|
+
tools: ProjectToolCatalogEntry[];
|
|
65
|
+
loadErrors: ProjectToolLoadError[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface LoadedCatalog extends ProjectToolCatalog {
|
|
69
|
+
definitions: Map<string, OperationDefinition>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ToolModule {
|
|
73
|
+
absolutePath: string;
|
|
74
|
+
sourcePath: string;
|
|
75
|
+
contributions: ProjectToolContribution[];
|
|
76
|
+
registrationError?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ToolContributionRegistration {
|
|
80
|
+
id: string;
|
|
81
|
+
point: ToolContributionPoint;
|
|
82
|
+
title: string;
|
|
83
|
+
entry: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type ToolRegistration =
|
|
87
|
+
| string
|
|
88
|
+
| {
|
|
89
|
+
entry: string;
|
|
90
|
+
contributions?: ToolContributionRegistration[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
interface PackageJsonShape {
|
|
94
|
+
name?: string;
|
|
95
|
+
dependencies?: Record<string, string>;
|
|
96
|
+
devDependencies?: Record<string, string>;
|
|
97
|
+
vgai?: { tools?: ToolRegistration[] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function readPackageJson(path: string): Promise<PackageJsonShape | null> {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(await readFile(path, 'utf8')) as PackageJsonShape;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveDependencyPackageJson(
|
|
109
|
+
requireFromProject: ReturnType<typeof createRequire>,
|
|
110
|
+
dependencyName: string,
|
|
111
|
+
): string | null {
|
|
112
|
+
try {
|
|
113
|
+
return requireFromProject.resolve(`${dependencyName}/package.json`);
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function registrationEntry(registration: ToolRegistration): string | null {
|
|
120
|
+
return typeof registration === 'string' ? registration : registration?.entry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function registrationContributions(
|
|
124
|
+
registration: ToolRegistration,
|
|
125
|
+
): ToolContributionRegistration[] | undefined {
|
|
126
|
+
return typeof registration === 'object' && registration !== null
|
|
127
|
+
? registration.contributions
|
|
128
|
+
: undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validRelativeModule(spec: unknown): spec is string {
|
|
132
|
+
return typeof spec === 'string' && spec.startsWith('./') && !spec.includes('..');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validContributionPoint(value: unknown): value is ToolContributionPoint {
|
|
136
|
+
return (
|
|
137
|
+
value === 'workspace.document' ||
|
|
138
|
+
value === 'selection.inspector' ||
|
|
139
|
+
value === 'workspace.utility'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one explicit manifest validator keeps every contribution field and teaching error at the package boundary.
|
|
144
|
+
function resolveContributions(
|
|
145
|
+
registrations: unknown,
|
|
146
|
+
root: string,
|
|
147
|
+
dependency: boolean,
|
|
148
|
+
): { contributions: ProjectToolContribution[]; error?: string } {
|
|
149
|
+
if (registrations === undefined) return { contributions: [] };
|
|
150
|
+
if (!Array.isArray(registrations)) {
|
|
151
|
+
return { contributions: [], error: '`contributions` must be an array' };
|
|
152
|
+
}
|
|
153
|
+
const contributions: ProjectToolContribution[] = [];
|
|
154
|
+
const ids = new Set<string>();
|
|
155
|
+
for (const [index, value] of registrations.entries()) {
|
|
156
|
+
if (!value || typeof value !== 'object') {
|
|
157
|
+
return { contributions: [], error: `contributions[${index}] must be an object` };
|
|
158
|
+
}
|
|
159
|
+
const candidate = value as Partial<ToolContributionRegistration>;
|
|
160
|
+
if (typeof candidate.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(candidate.id)) {
|
|
161
|
+
return {
|
|
162
|
+
contributions: [],
|
|
163
|
+
error: `contributions[${index}].id must be a lowercase kebab-case id`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (ids.has(candidate.id)) {
|
|
167
|
+
return {
|
|
168
|
+
contributions: [],
|
|
169
|
+
error: `duplicate contribution id ${JSON.stringify(candidate.id)}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (!validContributionPoint(candidate.point)) {
|
|
173
|
+
return {
|
|
174
|
+
contributions: [],
|
|
175
|
+
error:
|
|
176
|
+
`contributions[${index}].point must be ` +
|
|
177
|
+
`'workspace.document', 'selection.inspector', or 'workspace.utility'`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (typeof candidate.title !== 'string' || candidate.title.trim() === '') {
|
|
181
|
+
return { contributions: [], error: `contributions[${index}].title must be non-empty` };
|
|
182
|
+
}
|
|
183
|
+
if (!validRelativeModule(candidate.entry)) {
|
|
184
|
+
return {
|
|
185
|
+
contributions: [],
|
|
186
|
+
error: `contributions[${index}].entry must be a ./-relative module without parent traversal`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const absoluteEntry = resolve(root, candidate.entry);
|
|
190
|
+
if (!absoluteEntry.startsWith(`${root}${sep}`)) {
|
|
191
|
+
return {
|
|
192
|
+
contributions: [],
|
|
193
|
+
error: `contributions[${index}].entry resolves outside its package`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
ids.add(candidate.id);
|
|
197
|
+
contributions.push({
|
|
198
|
+
id: candidate.id,
|
|
199
|
+
point: candidate.point,
|
|
200
|
+
title: candidate.title.trim(),
|
|
201
|
+
entryPath: dependency ? absoluteEntry : candidate.entry.slice(2),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return { contributions };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function advertisedDependencyTools(
|
|
208
|
+
requireFromProject: ReturnType<typeof createRequire>,
|
|
209
|
+
dependencyName: string,
|
|
210
|
+
): Promise<ToolModule[]> {
|
|
211
|
+
const packageJsonPath = resolveDependencyPackageJson(requireFromProject, dependencyName);
|
|
212
|
+
if (!packageJsonPath) return [];
|
|
213
|
+
const dependencyPackage = await readPackageJson(packageJsonPath);
|
|
214
|
+
const registrations = dependencyPackage?.vgai?.tools;
|
|
215
|
+
if (!Array.isArray(registrations)) return [];
|
|
216
|
+
const packageRoot = dirname(packageJsonPath);
|
|
217
|
+
return registrations.map((registration, index): ToolModule => {
|
|
218
|
+
const entry = registrationEntry(registration);
|
|
219
|
+
const registrationPath = `${dependencyName}:package.json#vgai.tools[${index}]`;
|
|
220
|
+
if (!validRelativeModule(entry)) {
|
|
221
|
+
return {
|
|
222
|
+
absolutePath: packageJsonPath,
|
|
223
|
+
sourcePath: registrationPath,
|
|
224
|
+
contributions: [],
|
|
225
|
+
registrationError: 'must be a ./-relative tool module without parent traversal',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const absolutePath = resolve(packageRoot, entry);
|
|
229
|
+
if (!absolutePath.startsWith(`${packageRoot}${sep}`)) {
|
|
230
|
+
return {
|
|
231
|
+
absolutePath,
|
|
232
|
+
sourcePath: registrationPath,
|
|
233
|
+
contributions: [],
|
|
234
|
+
registrationError: 'resolves outside the dependency package',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const resolved = resolveContributions(
|
|
238
|
+
registrationContributions(registration),
|
|
239
|
+
packageRoot,
|
|
240
|
+
true,
|
|
241
|
+
);
|
|
242
|
+
if (resolved.error) {
|
|
243
|
+
return {
|
|
244
|
+
absolutePath,
|
|
245
|
+
sourcePath: registrationPath,
|
|
246
|
+
contributions: [],
|
|
247
|
+
registrationError: resolved.error,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
absolutePath,
|
|
252
|
+
sourcePath: `${dependencyName}:${entry.slice(2)}`,
|
|
253
|
+
contributions: resolved.contributions,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function dependencyToolModules(projectRoot: string): Promise<ToolModule[]> {
|
|
259
|
+
const projectPackage = await readPackageJson(resolve(projectRoot, 'package.json'));
|
|
260
|
+
if (!projectPackage) return [];
|
|
261
|
+
const dependencyNames = [
|
|
262
|
+
...Object.keys(projectPackage.dependencies ?? {}),
|
|
263
|
+
...Object.keys(projectPackage.devDependencies ?? {}),
|
|
264
|
+
].sort();
|
|
265
|
+
const requireFromProject = createRequire(resolve(projectRoot, 'package.json'));
|
|
266
|
+
return (
|
|
267
|
+
await Promise.all(
|
|
268
|
+
dependencyNames.map((name) => advertisedDependencyTools(requireFromProject, name)),
|
|
269
|
+
)
|
|
270
|
+
).flat();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function projectToolModules(projectRoot: string): Promise<ToolModule[]> {
|
|
274
|
+
const projectPackage = await readPackageJson(resolve(projectRoot, 'package.json'));
|
|
275
|
+
const registrations = projectPackage?.vgai?.tools;
|
|
276
|
+
if (!Array.isArray(registrations)) return [];
|
|
277
|
+
return registrations.map((registration, index): ToolModule => {
|
|
278
|
+
const entry = registrationEntry(registration);
|
|
279
|
+
const registrationPath = `package.json#vgai.tools[${index}]`;
|
|
280
|
+
if (!validRelativeModule(entry)) {
|
|
281
|
+
return {
|
|
282
|
+
absolutePath: resolve(projectRoot, 'package.json'),
|
|
283
|
+
sourcePath: registrationPath,
|
|
284
|
+
contributions: [],
|
|
285
|
+
registrationError: 'must be a ./-relative tool module without parent traversal',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const absolutePath = resolve(projectRoot, entry);
|
|
289
|
+
if (!absolutePath.startsWith(`${projectRoot}${sep}`)) {
|
|
290
|
+
return {
|
|
291
|
+
absolutePath,
|
|
292
|
+
sourcePath: registrationPath,
|
|
293
|
+
contributions: [],
|
|
294
|
+
registrationError: 'resolves outside the project',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const resolved = resolveContributions(
|
|
298
|
+
registrationContributions(registration),
|
|
299
|
+
projectRoot,
|
|
300
|
+
false,
|
|
301
|
+
);
|
|
302
|
+
if (resolved.error) {
|
|
303
|
+
return {
|
|
304
|
+
absolutePath,
|
|
305
|
+
sourcePath: registrationPath,
|
|
306
|
+
contributions: [],
|
|
307
|
+
registrationError: resolved.error,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
absolutePath,
|
|
312
|
+
sourcePath: entry.slice(2),
|
|
313
|
+
contributions: resolved.contributions,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function operationShape(value: unknown): value is OperationDefinition {
|
|
319
|
+
if (value === null || typeof value !== 'object') return false;
|
|
320
|
+
const candidate = value as Partial<OperationDefinition>;
|
|
321
|
+
return (
|
|
322
|
+
typeof candidate.name === 'string' &&
|
|
323
|
+
typeof candidate.summary === 'string' &&
|
|
324
|
+
typeof candidate.description === 'string' &&
|
|
325
|
+
candidate.input !== null &&
|
|
326
|
+
typeof candidate.input === 'object' &&
|
|
327
|
+
typeof candidate.input.safeParse === 'function' &&
|
|
328
|
+
candidate.result !== null &&
|
|
329
|
+
typeof candidate.result === 'object' &&
|
|
330
|
+
typeof candidate.result.safeParse === 'function' &&
|
|
331
|
+
Array.isArray(candidate.errors) &&
|
|
332
|
+
candidate.requires !== null &&
|
|
333
|
+
typeof candidate.requires === 'object' &&
|
|
334
|
+
(candidate.host === 'node' ||
|
|
335
|
+
candidate.host === 'editor-browser' ||
|
|
336
|
+
candidate.host === 'runtime-page') &&
|
|
337
|
+
typeof candidate.mutates === 'boolean' &&
|
|
338
|
+
typeof candidate.supportsDryRun === 'boolean' &&
|
|
339
|
+
candidate.permission !== null &&
|
|
340
|
+
typeof candidate.permission === 'object' &&
|
|
341
|
+
typeof candidate.impl === 'function'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function jsonSchema(schema: z.ZodType): unknown {
|
|
346
|
+
return z.toJSONSchema(schema);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function toCatalogEntry(
|
|
350
|
+
definition: OperationDefinition,
|
|
351
|
+
sourcePath: string,
|
|
352
|
+
contributions: ProjectToolContribution[],
|
|
353
|
+
): ProjectToolCatalogEntry {
|
|
354
|
+
return {
|
|
355
|
+
name: definition.name,
|
|
356
|
+
summary: definition.summary,
|
|
357
|
+
description: definition.description,
|
|
358
|
+
sourcePath,
|
|
359
|
+
inputSchema: jsonSchema(definition.input),
|
|
360
|
+
resultSchema: jsonSchema(definition.result),
|
|
361
|
+
errors: definition.errors.map((error) => ({
|
|
362
|
+
code: error.code,
|
|
363
|
+
summary: error.summary,
|
|
364
|
+
...(error.data ? { dataSchema: jsonSchema(error.data) } : {}),
|
|
365
|
+
})),
|
|
366
|
+
requires: { ...definition.requires },
|
|
367
|
+
host: definition.host,
|
|
368
|
+
mutates: definition.mutates,
|
|
369
|
+
supportsDryRun: definition.supportsDryRun,
|
|
370
|
+
longRunning: definition.longRunning === true,
|
|
371
|
+
permission: { ...definition.permission },
|
|
372
|
+
contributions,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function loadCatalog(
|
|
377
|
+
projectRoot: string,
|
|
378
|
+
loadModule: ProjectModuleLoader | undefined,
|
|
379
|
+
): Promise<LoadedCatalog> {
|
|
380
|
+
const definitions = new Map<string, OperationDefinition>();
|
|
381
|
+
const tools: ProjectToolCatalogEntry[] = [];
|
|
382
|
+
const loadErrors: ProjectToolLoadError[] = [];
|
|
383
|
+
const files = [
|
|
384
|
+
...(await projectToolModules(projectRoot)),
|
|
385
|
+
...(await dependencyToolModules(projectRoot)),
|
|
386
|
+
].sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
387
|
+
|
|
388
|
+
const loadableFiles = files.filter((file) => {
|
|
389
|
+
if (!file.registrationError) return true;
|
|
390
|
+
loadErrors.push({ sourcePath: file.sourcePath, message: file.registrationError });
|
|
391
|
+
return false;
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (loadableFiles.length > 0 && !loadModule) {
|
|
395
|
+
loadErrors.push({
|
|
396
|
+
sourcePath: 'package.json#vgai.tools',
|
|
397
|
+
message:
|
|
398
|
+
'This editor host cannot load project TypeScript modules. Use the Vite-backed dev or packaged editor.',
|
|
399
|
+
});
|
|
400
|
+
return { tools, loadErrors, definitions };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const registry = new OperationRegistry();
|
|
404
|
+
for (const { absolutePath, sourcePath, contributions } of loadableFiles) {
|
|
405
|
+
try {
|
|
406
|
+
const imported = (await loadModule?.(absolutePath)) as Record<string, unknown> | undefined;
|
|
407
|
+
const definition = imported?.['tool'];
|
|
408
|
+
if (!operationShape(definition)) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
'does not export a complete `tool` definition; export `tool = defineTool({...})` from @vgai/sdk/tools',
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (!definition.name.startsWith('project.')) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`declares ${JSON.stringify(definition.name)}; project-local tool names must start with "project."`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (definition.mutates && definition.permission.risk === 'read') {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`declares mutates:true with read risk; mutating operations must declare write or destructive risk`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
registry.register(definition);
|
|
424
|
+
const entry = toCatalogEntry(definition, sourcePath, contributions);
|
|
425
|
+
definitions.set(definition.name, definition);
|
|
426
|
+
tools.push(entry);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
loadErrors.push({
|
|
429
|
+
sourcePath,
|
|
430
|
+
message: error instanceof Error ? error.message : String(error),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
tools.sort((a, b) => a.name.localeCompare(b.name));
|
|
436
|
+
return { tools, loadErrors, definitions };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export async function discoverProjectTools(
|
|
440
|
+
projectRoot: string,
|
|
441
|
+
loadModule: ProjectModuleLoader | undefined,
|
|
442
|
+
): Promise<ProjectToolCatalog> {
|
|
443
|
+
const { definitions: _definitions, ...catalog } = await loadCatalog(projectRoot, loadModule);
|
|
444
|
+
return catalog;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export type ProjectToolExecution =
|
|
448
|
+
| { status: 200; body: OperationOutcome; changedProject: boolean }
|
|
449
|
+
| {
|
|
450
|
+
status: 404 | 409;
|
|
451
|
+
body: { ok: false; error: { code: string; message: string } };
|
|
452
|
+
changedProject: false;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
export async function executeProjectTool(options: {
|
|
456
|
+
projectRoot: string;
|
|
457
|
+
loadModule: ProjectModuleLoader | undefined;
|
|
458
|
+
name: string;
|
|
459
|
+
input: unknown;
|
|
460
|
+
confirmed: boolean;
|
|
461
|
+
signal?: AbortSignal;
|
|
462
|
+
}): Promise<ProjectToolExecution> {
|
|
463
|
+
const loaded = await loadCatalog(options.projectRoot, options.loadModule);
|
|
464
|
+
const definition = loaded.definitions.get(options.name);
|
|
465
|
+
if (!definition) {
|
|
466
|
+
return {
|
|
467
|
+
status: 404,
|
|
468
|
+
changedProject: false,
|
|
469
|
+
body: {
|
|
470
|
+
ok: false,
|
|
471
|
+
error: {
|
|
472
|
+
code: 'PROJECT_TOOL_NOT_FOUND',
|
|
473
|
+
message: `No registered project tool is named ${JSON.stringify(options.name)}.`,
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (definition.host !== 'node') {
|
|
479
|
+
return {
|
|
480
|
+
status: 409,
|
|
481
|
+
changedProject: false,
|
|
482
|
+
body: {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: {
|
|
485
|
+
code: 'PROJECT_TOOL_HOST_UNAVAILABLE',
|
|
486
|
+
message:
|
|
487
|
+
`Project tool ${JSON.stringify(options.name)} declares host ${JSON.stringify(definition.host)}. ` +
|
|
488
|
+
'This host currently executes Node-hosted project tools; call reusable browser/runtime functions directly.',
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if ((definition.mutates || definition.permission.risk !== 'read') && !options.confirmed) {
|
|
494
|
+
return {
|
|
495
|
+
status: 409,
|
|
496
|
+
changedProject: false,
|
|
497
|
+
body: {
|
|
498
|
+
ok: false,
|
|
499
|
+
error: {
|
|
500
|
+
code: 'PROJECT_TOOL_CONFIRMATION_REQUIRED',
|
|
501
|
+
message:
|
|
502
|
+
`${definition.permission.risk} tool ${JSON.stringify(options.name)} requires explicit confirmation: ` +
|
|
503
|
+
definition.permission.summary,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const registry = new OperationRegistry();
|
|
510
|
+
registry.register(definition);
|
|
511
|
+
const provenanceInput = definition.input.safeParse(options.input);
|
|
512
|
+
const operationSource = loaded.tools.find((entry) => entry.name === options.name)?.sourcePath;
|
|
513
|
+
const outcome = await runWithGenerativeExecutionCapture(() =>
|
|
514
|
+
registry.dispatch(options.name, options.input, {
|
|
515
|
+
projectRoot: options.projectRoot,
|
|
516
|
+
projectOutputs: createProjectOutputWriter(options.projectRoot, {
|
|
517
|
+
operationName: definition.name,
|
|
518
|
+
...(operationSource ? { operationSource } : {}),
|
|
519
|
+
...(provenanceInput.success ? { input: provenanceInput.data } : {}),
|
|
520
|
+
}),
|
|
521
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
return {
|
|
525
|
+
status: 200,
|
|
526
|
+
body: outcome,
|
|
527
|
+
changedProject: definition.mutates && outcome.ok,
|
|
528
|
+
};
|
|
529
|
+
}
|
package/server/server-utils.ts
CHANGED
|
@@ -336,7 +336,7 @@ export function validateManifestWrite(
|
|
|
336
336
|
* Which editor list a `src/**` add/unlink invalidates, or `null` if it's
|
|
337
337
|
* irrelevant. Feeds the editor-server's second `src/`-scoped chokidar
|
|
338
338
|
* watcher (`startWatcher()` in editor-server.ts) so a NEW
|
|
339
|
-
*
|
|
339
|
+
* registered tool modules or data/schema files show up
|
|
340
340
|
* in the dock/DATA tab without a full editor reload (spec §7 W4 field note
|
|
341
341
|
* c). `.schema.json` is included on the `data` side: its add/unlink flips the
|
|
342
342
|
* DATA tab between the no-emitted-schema teaching message and real widgets.
|
|
@@ -344,18 +344,21 @@ export function validateManifestWrite(
|
|
|
344
344
|
* `*.stories.tsx`/`*.stories.ts` anywhere under `src/` — the Stories tab.
|
|
345
345
|
*
|
|
346
346
|
* `relPath` is project-root-relative with forward slashes (same convention
|
|
347
|
-
* `/__editor/data-files`
|
|
347
|
+
* `/__editor/data-files` already uses).
|
|
348
348
|
*/
|
|
349
|
-
export function classifyProjectSrcPath(
|
|
350
|
-
relPath: string,
|
|
351
|
-
): 'tools' | 'data' | 'stories' | 'operations' | null {
|
|
352
|
-
if (relPath.startsWith('src/tools/') && relPath.endsWith('.tool.tsx')) return 'tools';
|
|
349
|
+
export function classifyProjectSrcPath(relPath: string): 'tools' | 'data' | 'stories' | null {
|
|
353
350
|
if (
|
|
354
|
-
relPath.startsWith('src/
|
|
355
|
-
(relPath.endsWith('.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
351
|
+
relPath.startsWith('src/tools/') &&
|
|
352
|
+
(relPath.endsWith('.tool.ts') ||
|
|
353
|
+
relPath.endsWith('.tool.js') ||
|
|
354
|
+
relPath.endsWith('.document.tsx') ||
|
|
355
|
+
relPath.endsWith('.document.jsx') ||
|
|
356
|
+
relPath.endsWith('.inspector.tsx') ||
|
|
357
|
+
relPath.endsWith('.inspector.jsx') ||
|
|
358
|
+
relPath.endsWith('.utility.tsx') ||
|
|
359
|
+
relPath.endsWith('.utility.jsx'))
|
|
360
|
+
)
|
|
361
|
+
return 'tools';
|
|
359
362
|
if (
|
|
360
363
|
relPath.startsWith('src/data/') &&
|
|
361
364
|
(relPath.endsWith('.data.json') || relPath.endsWith('.schema.json'))
|
package/src/EditorContext.tsx
CHANGED
|
@@ -18,9 +18,9 @@ import { createHmrStableReactContext } from './hmr-stable-react-context';
|
|
|
18
18
|
import { initProject } from './initial-scene';
|
|
19
19
|
import { type InteractiveEditScope, InteractiveEditScopeContext } from './interactive-edit-scope';
|
|
20
20
|
import { getCurrentProject } from './project-manager';
|
|
21
|
-
import { startProjectOperationDiscovery } from './project-operations';
|
|
22
21
|
import { startProjectStoryDiscovery } from './project-story-discovery';
|
|
23
|
-
import {
|
|
22
|
+
import { startProjectToolContributionDiscovery } from './project-tool-discovery';
|
|
23
|
+
import { startProjectToolCatalog } from './project-tools';
|
|
24
24
|
|
|
25
25
|
export interface EditorStats {
|
|
26
26
|
fps: number;
|
|
@@ -92,9 +92,9 @@ export function EditorProvider({ children }: { children: ReactNode }) {
|
|
|
92
92
|
// keeps re-running on project change / tool-file add/unlink) whether or
|
|
93
93
|
// not the Dockview workspace is mounted. WorkspaceDock only consumes the
|
|
94
94
|
// discovered registrations.
|
|
95
|
-
useEffect(() =>
|
|
95
|
+
useEffect(() => startProjectToolContributionDiscovery(), []);
|
|
96
96
|
|
|
97
|
-
useEffect(() =>
|
|
97
|
+
useEffect(() => startProjectToolCatalog(), []);
|
|
98
98
|
|
|
99
99
|
// CSF stories may exist without a currently mounted React root. Discover
|
|
100
100
|
// them at editor init so hierarchy/Inspector story affordances and restored
|
package/src/action-registry.ts
CHANGED
|
@@ -203,6 +203,18 @@ export function buildStaticActions(
|
|
|
203
203
|
category: 'action',
|
|
204
204
|
execute: () => store.setShadingMode('unlit'),
|
|
205
205
|
},
|
|
206
|
+
{
|
|
207
|
+
id: 'shading.normals',
|
|
208
|
+
label: 'Normal Shading',
|
|
209
|
+
category: 'action',
|
|
210
|
+
execute: () => store.setShadingMode('normals'),
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'shading.overdraw',
|
|
214
|
+
label: 'Overdraw Shading',
|
|
215
|
+
category: 'action',
|
|
216
|
+
execute: () => store.setShadingMode('overdraw'),
|
|
217
|
+
},
|
|
206
218
|
{
|
|
207
219
|
id: 'view.top',
|
|
208
220
|
label: 'Top View',
|
|
@@ -10,6 +10,7 @@ import type { ReactNode } from 'react';
|
|
|
10
10
|
import { useSyncExternalStore } from 'react';
|
|
11
11
|
import { themeVars } from './design-system';
|
|
12
12
|
import { createHmrRegistrationGroup } from './hmr-registration-group';
|
|
13
|
+
import { closeWorkspaceUtility } from './workspace-dock-controller';
|
|
13
14
|
import { registerWorkspaceStatus } from './workspace-status-registry';
|
|
14
15
|
import { registerWorkspaceUtility } from './workspace-utility-registry';
|
|
15
16
|
|
|
@@ -62,6 +63,14 @@ export function activateAssetEditorContext(value: AssetEditorContextValue): () =
|
|
|
62
63
|
if (active?.token !== token) return;
|
|
63
64
|
active = null;
|
|
64
65
|
notifyChanged();
|
|
66
|
+
// React cleans the outgoing document effect before activating the next
|
|
67
|
+
// document's effect. Defer one microtask so model → model keeps the same
|
|
68
|
+
// Controls tab, while model → scene/image closes the now-ownerless tab.
|
|
69
|
+
queueMicrotask(() => {
|
|
70
|
+
if (getActiveAssetEditorContext()?.controls == null) {
|
|
71
|
+
closeWorkspaceUtility(ASSET_EDITOR_CONTROLS_UTILITY_ID);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
65
74
|
};
|
|
66
75
|
}
|
|
67
76
|
|
package/src/asset-events.ts
CHANGED
|
@@ -110,11 +110,9 @@ export function connectSceneEvents(onSceneChanged: (event: SceneChangedEvent) =>
|
|
|
110
110
|
* Connect to the editor server's SSE endpoint for project TOOL file
|
|
111
111
|
* add/unlink events (W6a, docs/DATA-TOOLS-DESIGN.md §7 W4 field note c). The
|
|
112
112
|
* server's `src/`-scoped watcher (`editor-server.ts`'s `startWatcher()`)
|
|
113
|
-
* broadcasts `tool-files-changed` whenever a
|
|
114
|
-
* or removed
|
|
115
|
-
*
|
|
116
|
-
* cheap by design, same physics as the D3 "discovery IS the folder scan"
|
|
117
|
-
* rule the DATA tab already follows).
|
|
113
|
+
* broadcasts `tool-files-changed` whenever a registered tool or contribution
|
|
114
|
+
* source is added, changed, or removed. The event carries no payload of
|
|
115
|
+
* substance beyond the touched path; callers refresh package registrations.
|
|
118
116
|
*/
|
|
119
117
|
export function connectToolFileEvents(onChanged: () => void): () => void {
|
|
120
118
|
// Browser mode: no Node dev server / SSE — a project-src file add/remove
|
|
@@ -125,16 +123,6 @@ export function connectToolFileEvents(onChanged: () => void): () => void {
|
|
|
125
123
|
return () => source.close();
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
/** Project-operation add/change/unlink events. Operation definitions are
|
|
129
|
-
* Node-loaded, so all three changes trigger a fresh catalog read rather than
|
|
130
|
-
* relying on the browser-only project-tool HMR path. */
|
|
131
|
-
export function connectOperationFileEvents(onChanged: () => void): () => void {
|
|
132
|
-
if (getEditorMode() === 'browser') return () => {};
|
|
133
|
-
const source = connectEvents();
|
|
134
|
-
source.addEventListener('operation-files-changed', () => onChanged());
|
|
135
|
-
return () => source.close();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
126
|
/**
|
|
139
127
|
* Connect to the editor server's SSE endpoint for project DATA file
|
|
140
128
|
* add/unlink events (W6a). Mirrors `connectToolFileEvents` for
|
package/src/asset-selection.ts
CHANGED
|
@@ -36,6 +36,9 @@ export interface SelectedAsset {
|
|
|
36
36
|
readonly health?: 'healthy' | 'source-only' | 'unsupported' | 'stale-import';
|
|
37
37
|
readonly sourcePath?: string;
|
|
38
38
|
readonly selectionCount?: number;
|
|
39
|
+
readonly selectionTotalBytes?: number;
|
|
40
|
+
readonly selectionFormats?: readonly string[];
|
|
41
|
+
readonly selectionReimportableCount?: number;
|
|
39
42
|
readonly dependencies?: readonly string[];
|
|
40
43
|
readonly references?: readonly { ownerPath: string; jsonPath: string }[];
|
|
41
44
|
readonly healthCodes?: readonly string[];
|