@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.
Files changed (137) hide show
  1. package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
  2. package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
  3. package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
  4. package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
  5. package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
  6. package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
  7. package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
  8. package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
  9. package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
  10. package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
  11. package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
  12. package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
  13. package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
  14. package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
  15. package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
  16. package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
  17. package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
  18. package/dist/assets/index-DiUacMXR.css +1 -0
  19. package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
  20. package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
  21. package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
  22. package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
  23. package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/dist-server/packaged.mjs +27125 -1075
  26. package/package.json +2 -2
  27. package/server/asset-history-snapshots.ts +189 -0
  28. package/server/asset-library-routes.ts +404 -53
  29. package/server/catalog-import-recipe.ts +97 -0
  30. package/server/catalog-reimport.ts +141 -0
  31. package/server/cloud-asset-catalog.ts +71 -5
  32. package/server/dev.ts +1 -1
  33. package/server/editor-server.ts +106 -51
  34. package/server/editor-sse.ts +5 -0
  35. package/server/generative-execution-context.ts +45 -0
  36. package/server/model-import-conversion.ts +345 -0
  37. package/server/packaged.ts +1 -1
  38. package/server/project-hmr-files.ts +3 -5
  39. package/server/project-output-writer.ts +440 -36
  40. package/server/project-tools.ts +529 -0
  41. package/server/server-utils.ts +14 -11
  42. package/src/EditorContext.tsx +4 -4
  43. package/src/action-registry.ts +12 -0
  44. package/src/asset-editor-context.tsx +9 -0
  45. package/src/asset-events.ts +3 -15
  46. package/src/asset-selection.ts +3 -0
  47. package/src/asset-workflow/asset-capabilities.ts +40 -0
  48. package/src/asset-workflow/asset-import-jobs.ts +108 -0
  49. package/src/asset-workflow/asset-library-query.ts +8 -1
  50. package/src/asset-workflow/asset-workflow-quality.ts +2 -0
  51. package/src/asset-workflow/import-contract.ts +2 -0
  52. package/src/asset-workflow/model-inspection.ts +92 -1
  53. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  54. package/src/asset-workflow/project-asset-commands.ts +1 -0
  55. package/src/asset-workflow/project-asset-health.ts +3 -6
  56. package/src/asset-workflow/project-asset-operations.ts +119 -4
  57. package/src/command-listener.ts +37 -9
  58. package/src/components/ApplicationMenus.tsx +13 -15
  59. package/src/components/AssetBrowser.tsx +301 -26
  60. package/src/components/AssetImportDetails.tsx +207 -92
  61. package/src/components/AssetImportJobStatus.tsx +55 -0
  62. package/src/components/CenterDocuments.tsx +28 -1
  63. package/src/components/CommandPalette.tsx +2 -2
  64. package/src/components/DefaultEditorLayout.tsx +4 -0
  65. package/src/components/InspectorToolSection.tsx +12 -10
  66. package/src/components/OnlineAssetBrowser.tsx +197 -11
  67. package/src/components/ToolHost.tsx +8 -8
  68. package/src/components/ViewportOverlay.tsx +13 -26
  69. package/src/components/ViewportPanel.tsx +19 -14
  70. package/src/components/ViewportShadingMenu.tsx +103 -0
  71. package/src/components/WorkspaceDock.tsx +103 -17
  72. package/src/components/asset-documents.tsx +38 -11
  73. package/src/components/asset-editor-persistence.ts +300 -5
  74. package/src/components/asset-selection-section.tsx +83 -9
  75. package/src/components/asset-viewers/AudioViewer.tsx +4 -7
  76. package/src/components/asset-viewers/ModelViewer.tsx +168 -5
  77. package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
  78. package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
  79. package/src/components/asset-workflow.css +30 -3
  80. package/src/components/primitives/editor-icons.ts +1 -0
  81. package/src/components/project-tool-documents.tsx +298 -0
  82. package/src/components/status-contributions.tsx +38 -0
  83. package/src/components/tool-documents.tsx +20 -17
  84. package/src/components/tool-schema-form.tsx +258 -0
  85. package/src/components/workspace-dock.css +16 -0
  86. package/src/editor-api.ts +159 -22
  87. package/src/editor-store.ts +6 -5
  88. package/src/editor-viewport.ts +12 -7
  89. package/src/game-viewport-state.ts +28 -0
  90. package/src/model-thumbnail.ts +97 -9
  91. package/src/play-mode.ts +30 -3
  92. package/src/project-tool-discovery.ts +5 -5
  93. package/src/{project-operations.ts → project-tools.ts} +39 -26
  94. package/src/scene-sync.ts +0 -67
  95. package/src/storage/handle-storage.ts +5 -1
  96. package/src/storage/http-storage.ts +7 -2
  97. package/src/storage/mem-storage.ts +7 -1
  98. package/src/storage/types.ts +2 -0
  99. package/src/theme-css.ts +17 -7
  100. package/src/tool-loader.ts +152 -241
  101. package/src/viewport-shading-boundary.ts +15 -0
  102. package/src/widgets/index.ts +2 -1
  103. package/src/workspace-dock-controller.ts +8 -0
  104. package/src/workspace-document-registry.ts +4 -4
  105. package/src/workspace-state-persistence.ts +36 -10
  106. package/template/.claude/skills/editor/SKILL.md +13 -3
  107. package/template/AGENTS.md +100 -83
  108. package/template/package.json +16 -0
  109. package/template/public/scenes/default.vscn.json +1 -2
  110. package/template/scripts/playtest.ts +7 -37
  111. package/template/server/rooms/game-room.ts +2 -2
  112. package/template/src/data/README.md +3 -2
  113. package/template/src/data/tuning.data.json +1 -3
  114. package/template/src/data/tuning.schema.json +0 -12
  115. package/template/src/data/tuning.schema.ts +2 -9
  116. package/template/src/data/tuning.ts +1 -1
  117. package/template/src/scripts/registry.ts +0 -3
  118. package/template/src/tools/README.md +151 -16
  119. package/template/src/tools/autoplay.tool.ts +31 -0
  120. package/template/src/tools/autoplay.ts +15 -0
  121. package/template/src/tools/tuning.document.tsx +107 -0
  122. package/template/src/tools/tuning.tool.ts +24 -0
  123. package/template/src/tools/tuning.ts +9 -0
  124. package/template/src/ui/Button.stories.tsx +1 -1
  125. package/template/tests/acceptance/example.spec.ts +4 -59
  126. package/template/vgai.game.json +1 -1
  127. package/dist/assets/index-bAGhEnnO.css +0 -1
  128. package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
  129. package/dist/project-scripts.bundle.js +0 -3917
  130. package/server/project-operations.ts +0 -343
  131. package/src/components/project-operation-documents.tsx +0 -226
  132. package/template/src/operations/README.md +0 -88
  133. package/template/src/scripts/components/data-spinner.ts +0 -86
  134. package/template/src/scripts/components/spin-lap-math.ts +0 -27
  135. package/template/src/tools/example.tool.tsx +0 -97
  136. package/template/src/tools/spin.tool.tsx +0 -101
  137. package/template/tests/logic/example.test.ts +0 -31
@@ -1,343 +0,0 @@
1
- /**
2
- * Project-operation discovery and execution.
3
- *
4
- * Project modules stay ordinary TS/JS. This server-side boundary is the only
5
- * automatic loader: it recursively scans src/operations for operation modules, asks the
6
- * owning Vite server to load each module in Node, validates the exported
7
- * OperationDefinition, and dispatches through the existing SDK registry.
8
- * Nothing here imports an operation into the editor browser or a game build.
9
- */
10
-
11
- import type { Dirent } from 'node:fs';
12
- import { readdir, readFile } from 'node:fs/promises';
13
- import { createRequire } from 'node:module';
14
- import { dirname, relative, 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 { createProjectOutputWriter } from './project-output-writer';
19
-
20
- export type ProjectModuleLoader = (absoluteModuleId: string) => Promise<unknown>;
21
-
22
- export interface ProjectOperationErrorSummary {
23
- code: string;
24
- summary: string;
25
- dataSchema?: unknown;
26
- }
27
-
28
- export interface ProjectOperationCatalogEntry {
29
- name: string;
30
- summary: string;
31
- description: string;
32
- sourcePath: string;
33
- inputSchema: unknown;
34
- resultSchema: unknown;
35
- errors: ProjectOperationErrorSummary[];
36
- requires: Record<string, boolean | undefined>;
37
- host: 'node' | 'editor-browser' | 'runtime-page';
38
- mutates: boolean;
39
- supportsDryRun: boolean;
40
- longRunning: boolean;
41
- permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
42
- }
43
-
44
- export interface ProjectOperationLoadError {
45
- sourcePath: string;
46
- message: string;
47
- }
48
-
49
- export interface ProjectOperationCatalog {
50
- operations: ProjectOperationCatalogEntry[];
51
- loadErrors: ProjectOperationLoadError[];
52
- }
53
-
54
- interface LoadedCatalog extends ProjectOperationCatalog {
55
- definitions: Map<string, OperationDefinition>;
56
- }
57
-
58
- function projectPath(projectRoot: string, absolutePath: string): string {
59
- return relative(projectRoot, absolutePath).split(sep).join('/');
60
- }
61
-
62
- async function findOperationFiles(dir: string): Promise<string[]> {
63
- const found: string[] = [];
64
- let entries: Dirent[];
65
- try {
66
- entries = await readdir(dir, { withFileTypes: true });
67
- } catch {
68
- return found;
69
- }
70
- for (const entry of entries) {
71
- if (entry.name.startsWith('.')) continue;
72
- const absolute = resolve(dir, entry.name);
73
- if (entry.isDirectory()) {
74
- found.push(...(await findOperationFiles(absolute)));
75
- } else if (entry.name.endsWith('.operation.ts') || entry.name.endsWith('.operation.js')) {
76
- found.push(absolute);
77
- }
78
- }
79
- return found;
80
- }
81
-
82
- interface OperationFile {
83
- absolutePath: string;
84
- sourcePath: string;
85
- }
86
-
87
- interface PackageJsonShape {
88
- name?: string;
89
- dependencies?: Record<string, string>;
90
- devDependencies?: Record<string, string>;
91
- vgai?: { operations?: string[] };
92
- }
93
-
94
- async function readPackageJson(path: string): Promise<PackageJsonShape | null> {
95
- try {
96
- return JSON.parse(await readFile(path, 'utf8')) as PackageJsonShape;
97
- } catch {
98
- return null;
99
- }
100
- }
101
-
102
- function resolveDependencyPackageJson(
103
- requireFromProject: ReturnType<typeof createRequire>,
104
- dependencyName: string,
105
- ): string | null {
106
- try {
107
- return requireFromProject.resolve(`${dependencyName}/package.json`);
108
- } catch {
109
- return null;
110
- }
111
- }
112
-
113
- async function advertisedDependencyOperations(
114
- requireFromProject: ReturnType<typeof createRequire>,
115
- dependencyName: string,
116
- ): Promise<OperationFile[]> {
117
- const packageJsonPath = resolveDependencyPackageJson(requireFromProject, dependencyName);
118
- if (!packageJsonPath) return [];
119
- const dependencyPackage = await readPackageJson(packageJsonPath);
120
- const specs = dependencyPackage?.vgai?.operations;
121
- if (!Array.isArray(specs)) return [];
122
- const packageRoot = dirname(packageJsonPath);
123
- return specs.flatMap((spec): OperationFile[] => {
124
- if (typeof spec !== 'string' || !spec.startsWith('./')) return [];
125
- const absolutePath = resolve(packageRoot, spec);
126
- if (!absolutePath.startsWith(`${packageRoot}${sep}`)) return [];
127
- return [{ absolutePath, sourcePath: `${dependencyName}:${spec.slice(2)}` }];
128
- });
129
- }
130
-
131
- async function dependencyOperationFiles(projectRoot: string): Promise<OperationFile[]> {
132
- const projectPackage = await readPackageJson(resolve(projectRoot, 'package.json'));
133
- if (!projectPackage) return [];
134
- const dependencyNames = [
135
- ...Object.keys(projectPackage.dependencies ?? {}),
136
- ...Object.keys(projectPackage.devDependencies ?? {}),
137
- ].sort();
138
- const requireFromProject = createRequire(resolve(projectRoot, 'package.json'));
139
- return (
140
- await Promise.all(
141
- dependencyNames.map((name) => advertisedDependencyOperations(requireFromProject, name)),
142
- )
143
- ).flat();
144
- }
145
-
146
- function operationShape(value: unknown): value is OperationDefinition {
147
- if (value === null || typeof value !== 'object') return false;
148
- const candidate = value as Partial<OperationDefinition>;
149
- return (
150
- typeof candidate.name === 'string' &&
151
- typeof candidate.summary === 'string' &&
152
- typeof candidate.description === 'string' &&
153
- candidate.input !== null &&
154
- typeof candidate.input === 'object' &&
155
- typeof candidate.input.safeParse === 'function' &&
156
- candidate.result !== null &&
157
- typeof candidate.result === 'object' &&
158
- typeof candidate.result.safeParse === 'function' &&
159
- Array.isArray(candidate.errors) &&
160
- candidate.requires !== null &&
161
- typeof candidate.requires === 'object' &&
162
- (candidate.host === 'node' ||
163
- candidate.host === 'editor-browser' ||
164
- candidate.host === 'runtime-page') &&
165
- typeof candidate.mutates === 'boolean' &&
166
- typeof candidate.supportsDryRun === 'boolean' &&
167
- candidate.permission !== null &&
168
- typeof candidate.permission === 'object' &&
169
- typeof candidate.impl === 'function'
170
- );
171
- }
172
-
173
- function jsonSchema(schema: z.ZodType): unknown {
174
- return z.toJSONSchema(schema);
175
- }
176
-
177
- function toCatalogEntry(
178
- definition: OperationDefinition,
179
- sourcePath: string,
180
- ): ProjectOperationCatalogEntry {
181
- return {
182
- name: definition.name,
183
- summary: definition.summary,
184
- description: definition.description,
185
- sourcePath,
186
- inputSchema: jsonSchema(definition.input),
187
- resultSchema: jsonSchema(definition.result),
188
- errors: definition.errors.map((error) => ({
189
- code: error.code,
190
- summary: error.summary,
191
- ...(error.data ? { dataSchema: jsonSchema(error.data) } : {}),
192
- })),
193
- requires: { ...definition.requires },
194
- host: definition.host,
195
- mutates: definition.mutates,
196
- supportsDryRun: definition.supportsDryRun,
197
- longRunning: definition.longRunning === true,
198
- permission: { ...definition.permission },
199
- };
200
- }
201
-
202
- async function loadCatalog(
203
- projectRoot: string,
204
- loadModule: ProjectModuleLoader | undefined,
205
- ): Promise<LoadedCatalog> {
206
- const definitions = new Map<string, OperationDefinition>();
207
- const operations: ProjectOperationCatalogEntry[] = [];
208
- const loadErrors: ProjectOperationLoadError[] = [];
209
- const projectFiles = (await findOperationFiles(resolve(projectRoot, 'src', 'operations')))
210
- .sort()
211
- .map((absolutePath) => ({ absolutePath, sourcePath: projectPath(projectRoot, absolutePath) }));
212
- const files = [...projectFiles, ...(await dependencyOperationFiles(projectRoot))].sort((a, b) =>
213
- a.sourcePath.localeCompare(b.sourcePath),
214
- );
215
-
216
- if (files.length > 0 && !loadModule) {
217
- loadErrors.push({
218
- sourcePath: 'src/operations',
219
- message:
220
- 'This editor host cannot load project TypeScript modules. Use the Vite-backed dev or packaged editor.',
221
- });
222
- return { operations, loadErrors, definitions };
223
- }
224
-
225
- const registry = new OperationRegistry();
226
- for (const { absolutePath, sourcePath } of files) {
227
- try {
228
- const imported = (await loadModule?.(absolutePath)) as Record<string, unknown> | undefined;
229
- const definition = imported?.['operation'];
230
- if (!operationShape(definition)) {
231
- throw new Error(
232
- 'does not export a complete `operation` definition; export `operation = defineOperation({...})` from @vgai/sdk/registry',
233
- );
234
- }
235
- if (!definition.name.startsWith('project.')) {
236
- throw new Error(
237
- `declares ${JSON.stringify(definition.name)}; project-local operation names must start with "project."`,
238
- );
239
- }
240
- if (definition.mutates && definition.permission.risk === 'read') {
241
- throw new Error(
242
- `declares mutates:true with read risk; mutating operations must declare write or destructive risk`,
243
- );
244
- }
245
- registry.register(definition);
246
- const entry = toCatalogEntry(definition, sourcePath);
247
- definitions.set(definition.name, definition);
248
- operations.push(entry);
249
- } catch (error) {
250
- loadErrors.push({
251
- sourcePath,
252
- message: error instanceof Error ? error.message : String(error),
253
- });
254
- }
255
- }
256
-
257
- operations.sort((a, b) => a.name.localeCompare(b.name));
258
- return { operations, loadErrors, definitions };
259
- }
260
-
261
- export async function discoverProjectOperations(
262
- projectRoot: string,
263
- loadModule: ProjectModuleLoader | undefined,
264
- ): Promise<ProjectOperationCatalog> {
265
- const { definitions: _definitions, ...catalog } = await loadCatalog(projectRoot, loadModule);
266
- return catalog;
267
- }
268
-
269
- export type ProjectOperationExecution =
270
- | { status: 200; body: OperationOutcome; changedProject: boolean }
271
- | {
272
- status: 404 | 409;
273
- body: { ok: false; error: { code: string; message: string } };
274
- changedProject: false;
275
- };
276
-
277
- export async function executeProjectOperation(options: {
278
- projectRoot: string;
279
- loadModule: ProjectModuleLoader | undefined;
280
- name: string;
281
- input: unknown;
282
- confirmed: boolean;
283
- signal?: AbortSignal;
284
- }): Promise<ProjectOperationExecution> {
285
- const loaded = await loadCatalog(options.projectRoot, options.loadModule);
286
- const definition = loaded.definitions.get(options.name);
287
- if (!definition) {
288
- return {
289
- status: 404,
290
- changedProject: false,
291
- body: {
292
- ok: false,
293
- error: {
294
- code: 'PROJECT_OPERATION_NOT_FOUND',
295
- message: `No loaded project operation is named ${JSON.stringify(options.name)}.`,
296
- },
297
- },
298
- };
299
- }
300
- if (definition.host !== 'node') {
301
- return {
302
- status: 409,
303
- changedProject: false,
304
- body: {
305
- ok: false,
306
- error: {
307
- code: 'PROJECT_OPERATION_HOST_UNAVAILABLE',
308
- message:
309
- `Project operation ${JSON.stringify(options.name)} declares host ${JSON.stringify(definition.host)}. ` +
310
- 'Slice 1 executes Node-hosted project operations; call reusable browser/runtime functions directly.',
311
- },
312
- },
313
- };
314
- }
315
- if ((definition.mutates || definition.permission.risk !== 'read') && !options.confirmed) {
316
- return {
317
- status: 409,
318
- changedProject: false,
319
- body: {
320
- ok: false,
321
- error: {
322
- code: 'PROJECT_OPERATION_CONFIRMATION_REQUIRED',
323
- message:
324
- `${definition.permission.risk} operation ${JSON.stringify(options.name)} requires explicit confirmation: ` +
325
- definition.permission.summary,
326
- },
327
- },
328
- };
329
- }
330
-
331
- const registry = new OperationRegistry();
332
- registry.register(definition);
333
- const outcome = await registry.dispatch(options.name, options.input, {
334
- projectRoot: options.projectRoot,
335
- projectOutputs: createProjectOutputWriter(options.projectRoot),
336
- ...(options.signal ? { signal: options.signal } : {}),
337
- });
338
- return {
339
- status: 200,
340
- body: outcome,
341
- changedProject: definition.mutates && outcome.ok,
342
- };
343
- }
@@ -1,226 +0,0 @@
1
- import { useMemo, useState, useSyncExternalStore } from 'react';
2
- import type { EditorAction } from '../action-registry';
3
- import { Button, bg, border, danger, fontSize, radius, text } from '../design-system';
4
- import {
5
- getProjectOperations,
6
- projectOperationsVersion,
7
- runProjectOperation,
8
- subscribeProjectOperations,
9
- } from '../project-operations';
10
- import {
11
- openWorkspaceDocument,
12
- type WorkspaceDocumentContentProps,
13
- } from '../workspace-document-registry';
14
-
15
- const CATALOG_DOCUMENT_ID = 'project-operations';
16
- const OPERATION_DOCUMENT_PREFIX = 'project-operation:';
17
-
18
- export interface ProjectOperationDocumentStore {
19
- setActiveViewportTab(tab: 'scene' | 'game'): void;
20
- }
21
-
22
- function operationDocumentId(name: string): string {
23
- return `${OPERATION_DOCUMENT_PREFIX}${name}`;
24
- }
25
-
26
- export function openProjectOperationsDocument(store: ProjectOperationDocumentStore): void {
27
- openWorkspaceDocument({
28
- id: CATALOG_DOCUMENT_ID,
29
- title: 'Project Commands',
30
- kind: 'operation',
31
- Content: ProjectOperationDocumentContent,
32
- closeable: true,
33
- onActivate: () => store.setActiveViewportTab('scene'),
34
- });
35
- }
36
-
37
- export function openProjectOperationDocument(
38
- store: ProjectOperationDocumentStore | null,
39
- name: string,
40
- ): boolean {
41
- const operation = getProjectOperations().operations.find((entry) => entry.name === name);
42
- if (!operation) return false;
43
- openWorkspaceDocument({
44
- id: operationDocumentId(name),
45
- title: operation.summary || operation.name,
46
- kind: 'operation',
47
- provenance: { sourcePath: operation.sourcePath, origin: operation.name },
48
- Content: ProjectOperationDocumentContent,
49
- closeable: true,
50
- ...(store ? { onActivate: () => store.setActiveViewportTab('scene') } : {}),
51
- });
52
- return true;
53
- }
54
-
55
- export function buildProjectOperationActions(store: ProjectOperationDocumentStore): EditorAction[] {
56
- return [
57
- {
58
- id: 'project-operations:open',
59
- label: 'Open Project Commands',
60
- category: 'action',
61
- execute: () => openProjectOperationsDocument(store),
62
- },
63
- ...getProjectOperations().operations.map((operation) => ({
64
- id: `project-operation:${operation.name}`,
65
- label: `Run: ${operation.summary || operation.name}`,
66
- category: 'action' as const,
67
- execute: () => void openProjectOperationDocument(store, operation.name),
68
- })),
69
- ];
70
- }
71
-
72
- function JsonBlock({ value }: { value: unknown }) {
73
- return <pre style={jsonStyle}>{JSON.stringify(value, null, 2)}</pre>;
74
- }
75
-
76
- export function ProjectOperationDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
77
- useSyncExternalStore(subscribeProjectOperations, projectOperationsVersion);
78
- const snapshot = getProjectOperations();
79
- const operationName = documentId.startsWith(OPERATION_DOCUMENT_PREFIX)
80
- ? documentId.slice(OPERATION_DOCUMENT_PREFIX.length)
81
- : null;
82
- const operation = operationName
83
- ? snapshot.operations.find((entry) => entry.name === operationName)
84
- : null;
85
- const [inputText, setInputText] = useState('{}');
86
- const [result, setResult] = useState<unknown>(null);
87
- const [running, setRunning] = useState(false);
88
- const [inputError, setInputError] = useState<string | null>(null);
89
- const parsedSchema = useMemo(() => operation?.inputSchema ?? {}, [operation]);
90
-
91
- if (!operationName) {
92
- return (
93
- <div style={rootStyle} data-testid="project-operations-catalog">
94
- <h2 style={headingStyle}>Project Commands</h2>
95
- <p style={mutedStyle}>
96
- Auto-discovered from nested <code>src/operations/*.operation.ts</code> files. Commands are
97
- ordinary project functions with a validated tooling boundary; custom interfaces live in{' '}
98
- <code>src/tools/</code>.
99
- </p>
100
- {snapshot.operations.map((entry) => (
101
- <Button
102
- key={entry.name}
103
- type="button"
104
- className="vgai-operation-card"
105
- onClick={() => openProjectOperationDocument(null, entry.name)}
106
- data-testid={`project-command-card:${entry.name}`}
107
- >
108
- <div style={{ color: text[1] }}>{entry.summary}</div>
109
- <code style={mutedStyle}>{entry.name}</code>
110
- <div style={mutedStyle}>
111
- {entry.host} · {entry.permission.risk} · {entry.sourcePath}
112
- </div>
113
- </Button>
114
- ))}
115
- {snapshot.operations.length === 0 && snapshot.loadErrors.length === 0 && (
116
- <div style={mutedStyle}>No project commands found.</div>
117
- )}
118
- {snapshot.loadErrors.map((error) => (
119
- <div key={`${error.sourcePath}:${error.message}`} style={errorStyle}>
120
- {error.sourcePath}: {error.message}
121
- </div>
122
- ))}
123
- </div>
124
- );
125
- }
126
-
127
- if (!operation) {
128
- return <div style={rootStyle}>This operation is no longer available.</div>;
129
- }
130
-
131
- const runnable = operation.host === 'node';
132
- const execute = async () => {
133
- let input: unknown;
134
- try {
135
- input = JSON.parse(inputText);
136
- setInputError(null);
137
- } catch (error) {
138
- setInputError(error instanceof Error ? error.message : String(error));
139
- return;
140
- }
141
- const confirm =
142
- operation.permission.risk === 'read' ||
143
- window.confirm(`${operation.permission.summary}\n\nRun ${operation.name}?`);
144
- if (!confirm) return;
145
- setRunning(true);
146
- try {
147
- setResult(await runProjectOperation(operation.name, input, { confirm: true }));
148
- } catch (error) {
149
- setResult({ ok: false, error: { code: 'REQUEST_FAILED', message: String(error) } });
150
- } finally {
151
- setRunning(false);
152
- }
153
- };
154
-
155
- return (
156
- <div style={rootStyle} data-testid={`project-operation:${operation.name}`}>
157
- <h2 style={headingStyle}>{operation.summary}</h2>
158
- <code style={{ color: text[2] }}>{operation.name}</code>
159
- <p style={{ color: text[2] }}>{operation.description}</p>
160
- <div style={mutedStyle}>
161
- {operation.host} · {operation.permission.risk} · {operation.sourcePath}
162
- </div>
163
- {!runnable && (
164
- <div style={errorStyle}>
165
- This command runner supports Node-hosted operations. Import the underlying function
166
- directly in browser or runtime code.
167
- </div>
168
- )}
169
- <label style={labelStyle} htmlFor={`operation-input:${operation.name}`}>
170
- Input JSON
171
- </label>
172
- <textarea
173
- id={`operation-input:${operation.name}`}
174
- value={inputText}
175
- onChange={(event) => setInputText(event.target.value)}
176
- rows={10}
177
- style={textareaStyle}
178
- />
179
- {inputError && <div style={errorStyle}>{inputError}</div>}
180
- <Button type="button" disabled={!runnable || running} onClick={() => void execute()}>
181
- {running ? 'Running…' : 'Run command'}
182
- </Button>
183
- <details>
184
- <summary style={labelStyle}>Input schema</summary>
185
- <JsonBlock value={parsedSchema} />
186
- </details>
187
- {result !== null && (
188
- <section>
189
- <div style={labelStyle}>Result</div>
190
- <JsonBlock value={result} />
191
- </section>
192
- )}
193
- </div>
194
- );
195
- }
196
-
197
- const rootStyle: React.CSSProperties = {
198
- padding: 16,
199
- overflow: 'auto',
200
- pointerEvents: 'auto',
201
- color: text[1],
202
- fontSize: fontSize.base,
203
- };
204
- const headingStyle: React.CSSProperties = { margin: '0 0 8px', fontSize: 18 };
205
- const mutedStyle: React.CSSProperties = { color: text[2], fontSize: fontSize.sm };
206
- const labelStyle: React.CSSProperties = { display: 'block', margin: '14px 0 6px', color: text[1] };
207
- const errorStyle: React.CSSProperties = { marginTop: 10, color: danger, whiteSpace: 'pre-wrap' };
208
- const textareaStyle: React.CSSProperties = {
209
- width: '100%',
210
- boxSizing: 'border-box',
211
- padding: 10,
212
- resize: 'vertical',
213
- background: bg.inset,
214
- color: text[1],
215
- border: `1px solid ${border[2]}`,
216
- borderRadius: radius.sm,
217
- fontFamily: 'monospace',
218
- };
219
- const jsonStyle: React.CSSProperties = {
220
- overflow: 'auto',
221
- padding: 10,
222
- background: bg.inset,
223
- border: `1px solid ${border[1]}`,
224
- borderRadius: radius.sm,
225
- color: text[2],
226
- };
@@ -1,88 +0,0 @@
1
- # src/operations — discoverable project scripts
2
-
3
- Put tooling-facing operations in `*.operation.ts` files here. They remain
4
- ordinary TypeScript modules. Keep reusable implementation in a normal project
5
- module, then wrap it with one thin `operation` definition.
6
-
7
- ```ts
8
- // src/automation/inspect-something.ts
9
- export async function inspectSomething(input: { path: string }) {
10
- return { path: input.path };
11
- }
12
- ```
13
-
14
- ```ts
15
- // src/operations/inspect-something.operation.ts
16
- import { defineOperation } from '@vgai/sdk/registry';
17
- import { z } from 'zod';
18
- import { inspectSomething } from '../automation/inspect-something';
19
-
20
- export const operation = defineOperation({
21
- name: 'project.inspectSomething',
22
- summary: 'Inspect something owned by this project.',
23
- description: 'A project-local operation. The implementation remains directly importable.',
24
- input: z.object({ path: z.string() }),
25
- result: z.object({ path: z.string() }),
26
- errors: [],
27
- requires: { project: true },
28
- host: 'node',
29
- mutates: false,
30
- supportsDryRun: false,
31
- permission: { risk: 'read', summary: 'Reads project-local information.' },
32
- impl: inspectSomething,
33
- });
34
- ```
35
-
36
- The editor discovers these files without a registry. List them for agents with
37
- `npm run vgai -- run --list --json`; inspect one with
38
- `npm run vgai -- run project.inspectSomething --describe`; run it with
39
- `npm run vgai -- run project.inspectSomething --input input.json` while the
40
- project editor is running. The editor also provides a directly clickable
41
- Project Commands catalog and a default JSON/schema UI.
42
-
43
- For a frequent workflow, an ordinary package script may be a shortcut:
44
-
45
- ```json
46
- {
47
- "scripts": {
48
- "inspect-project": "npm run vgai -- run project.inspectSomething"
49
- }
50
- }
51
- ```
52
-
53
- That script is convenience, not registration; deleting it does not remove the
54
- capability from the editor or catalog.
55
-
56
- For a custom UI, add an ordinary `src/tools/*.tool.tsx` project tool and call
57
- the same operation through the public editor SDK:
58
-
59
- ```ts
60
- import { EditorClient } from '@vgai/editor-sdk';
61
-
62
- const editor = new EditorClient({ url: window.location.origin });
63
- const result = await editor.runProjectOperation('project.buildCharacter', input, {
64
- confirm: true,
65
- });
66
- if (result.ok) await editor.openAsset('/characters/generated.glb', 'model');
67
- ```
68
-
69
- Keep React/editor imports out of the operation module. If the operation writes
70
- ordinary files under `public/`, a successful declared mutation refreshes the
71
- project's Assets navigator automatically. The generator is a tool/command; its
72
- persisted files are assets. It does not become an Asset Library tile.
73
-
74
- Game/runtime code should import the underlying function directly rather than
75
- calling through the tooling registry.
76
-
77
- Installed packages can advertise operations from their own `package.json`
78
- using `{"vgai":{"operations":["./path/to/file.operation.js"]}}`. The editor
79
- loads direct dependencies only and reports the package-qualified source path.
80
- This is how a library can provide an optional author-time bake/import command
81
- without adding a domain switch to the editor or copying wrappers into every
82
- project.
83
-
84
- Node operations that generate assets receive `ctx.projectOutputs`. Hand it a
85
- complete batch of ordinary files under `public/`; the host validates all paths,
86
- stages the batch, commits it atomically, and returns a JSON-safe output
87
- manifest. Use `{ dryRun: true }` to validate without writing. Successful writes
88
- refresh the Assets navigator automatically.