@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,298 @@
|
|
|
1
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
2
|
+
import type { EditorAction } from '../action-registry';
|
|
3
|
+
import { Button, bg, border, danger, EditorBadge, fontSize, radius, text } from '../design-system';
|
|
4
|
+
import {
|
|
5
|
+
getProjectTools,
|
|
6
|
+
projectToolsVersion,
|
|
7
|
+
runProjectTool,
|
|
8
|
+
subscribeProjectTools,
|
|
9
|
+
} from '../project-tools';
|
|
10
|
+
import { getDocumentToolContributions } from '../tool-loader';
|
|
11
|
+
import {
|
|
12
|
+
openWorkspaceDocument,
|
|
13
|
+
type WorkspaceDocumentContentProps,
|
|
14
|
+
} from '../workspace-document-registry';
|
|
15
|
+
import { openToolDocument } from './tool-documents';
|
|
16
|
+
import { defaultValueForSchema, ToolSchemaForm } from './tool-schema-form';
|
|
17
|
+
|
|
18
|
+
export const PROJECT_TOOLS_DOCUMENT_ID = 'project-tools';
|
|
19
|
+
export const PROJECT_TOOL_DOCUMENT_PREFIX = 'project-tool:';
|
|
20
|
+
|
|
21
|
+
export interface ProjectToolDocumentStore {
|
|
22
|
+
setActiveViewportTab(tab: 'scene' | 'game'): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function projectToolDocumentId(name: string): string {
|
|
26
|
+
return `${PROJECT_TOOL_DOCUMENT_PREFIX}${name}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function openProjectToolsDocument(store: ProjectToolDocumentStore): void {
|
|
30
|
+
openWorkspaceDocument({
|
|
31
|
+
id: PROJECT_TOOLS_DOCUMENT_ID,
|
|
32
|
+
title: 'Project Tools',
|
|
33
|
+
kind: 'project-tool',
|
|
34
|
+
Content: ProjectToolDocumentContent,
|
|
35
|
+
closeable: true,
|
|
36
|
+
onActivate: () => store.setActiveViewportTab('scene'),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function openProjectToolDocument(
|
|
41
|
+
store: ProjectToolDocumentStore | null,
|
|
42
|
+
name: string,
|
|
43
|
+
): boolean {
|
|
44
|
+
const tool = getProjectTools().tools.find((entry) => entry.name === name);
|
|
45
|
+
if (!tool) return false;
|
|
46
|
+
const contribution = getDocumentToolContributions().find((item) => item.tool.name === name);
|
|
47
|
+
if (contribution) return openToolDocument(store, contribution.id);
|
|
48
|
+
openWorkspaceDocument({
|
|
49
|
+
id: projectToolDocumentId(name),
|
|
50
|
+
title: tool.summary || tool.name,
|
|
51
|
+
kind: 'project-tool',
|
|
52
|
+
provenance: { sourcePath: tool.sourcePath, origin: tool.name },
|
|
53
|
+
Content: ProjectToolDocumentContent,
|
|
54
|
+
closeable: true,
|
|
55
|
+
...(store ? { onActivate: () => store.setActiveViewportTab('scene') } : {}),
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildProjectToolActions(store: ProjectToolDocumentStore): EditorAction[] {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
id: 'project-tools:open',
|
|
64
|
+
label: 'Open Project Tools',
|
|
65
|
+
category: 'action',
|
|
66
|
+
execute: () => openProjectToolsDocument(store),
|
|
67
|
+
},
|
|
68
|
+
...getProjectTools().tools.map((tool) => ({
|
|
69
|
+
id: `project-tool:${tool.name}`,
|
|
70
|
+
label: `Run: ${tool.summary || tool.name}`,
|
|
71
|
+
category: 'action' as const,
|
|
72
|
+
execute: () => void openProjectToolDocument(store, tool.name),
|
|
73
|
+
})),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function JsonBlock({ value }: { value: unknown }) {
|
|
78
|
+
return <pre style={jsonStyle}>{JSON.stringify(value, null, 2)}</pre>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function ProjectToolDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
|
|
82
|
+
useSyncExternalStore(subscribeProjectTools, projectToolsVersion);
|
|
83
|
+
const snapshot = getProjectTools();
|
|
84
|
+
const toolName = documentId.startsWith(PROJECT_TOOL_DOCUMENT_PREFIX)
|
|
85
|
+
? documentId.slice(PROJECT_TOOL_DOCUMENT_PREFIX.length)
|
|
86
|
+
: null;
|
|
87
|
+
const tool = toolName ? snapshot.tools.find((entry) => entry.name === toolName) : null;
|
|
88
|
+
if (!toolName) {
|
|
89
|
+
return (
|
|
90
|
+
<div style={rootStyle} data-testid="project-tools-catalog">
|
|
91
|
+
<h2 style={headingStyle}>Project Tools</h2>
|
|
92
|
+
<p style={{ ...mutedStyle, margin: '0 0 18px', maxWidth: 720, lineHeight: 1.5 }}>
|
|
93
|
+
Explicitly registered in <code>package.json#vgai.tools</code>. Each tool is an ordinary
|
|
94
|
+
project function; a registration may pair it with optional editor contributions.
|
|
95
|
+
</p>
|
|
96
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
97
|
+
{snapshot.tools.map((entry) => (
|
|
98
|
+
<Button
|
|
99
|
+
key={entry.name}
|
|
100
|
+
variant="ghost"
|
|
101
|
+
className="vgai-project-tool-card"
|
|
102
|
+
onClick={() => openProjectToolDocument(null, entry.name)}
|
|
103
|
+
data-testid={`project-command-card:${entry.name}`}
|
|
104
|
+
>
|
|
105
|
+
<span style={cardTitleRowStyle}>
|
|
106
|
+
<strong style={{ color: text[1], fontWeight: 600 }}>{entry.summary}</strong>
|
|
107
|
+
<span style={openLabelStyle}>Open</span>
|
|
108
|
+
</span>
|
|
109
|
+
<code style={{ color: text[2], fontSize: fontSize.sm }}>{entry.name}</code>
|
|
110
|
+
<span style={cardMetaRowStyle}>
|
|
111
|
+
<span>
|
|
112
|
+
{entry.host} · {entry.permission.risk} · {entry.sourcePath}
|
|
113
|
+
</span>
|
|
114
|
+
{entry.contributions.length > 0 && (
|
|
115
|
+
<EditorBadge style={badgeStyle}>UI ×{entry.contributions.length}</EditorBadge>
|
|
116
|
+
)}
|
|
117
|
+
</span>
|
|
118
|
+
</Button>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
{snapshot.tools.length === 0 && snapshot.loadErrors.length === 0 && (
|
|
122
|
+
<div style={mutedStyle}>No project tools registered.</div>
|
|
123
|
+
)}
|
|
124
|
+
{snapshot.loadErrors.map((error) => (
|
|
125
|
+
<div key={`${error.sourcePath}:${error.message}`} style={errorStyle}>
|
|
126
|
+
{error.sourcePath}: {error.message}
|
|
127
|
+
</div>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!tool) {
|
|
134
|
+
return <div style={rootStyle}>This tool is no longer available.</div>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return <ProjectToolRunner key={tool.name} tool={tool} />;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ProjectToolRunner({
|
|
141
|
+
tool,
|
|
142
|
+
}: {
|
|
143
|
+
tool: ReturnType<typeof getProjectTools>['tools'][number];
|
|
144
|
+
}) {
|
|
145
|
+
const initial = defaultValueForSchema(tool.inputSchema);
|
|
146
|
+
const [input, setInput] = useState<Record<string, unknown>>(
|
|
147
|
+
initial && typeof initial === 'object' && !Array.isArray(initial)
|
|
148
|
+
? (initial as Record<string, unknown>)
|
|
149
|
+
: {},
|
|
150
|
+
);
|
|
151
|
+
const [rawText, setRawText] = useState(() => JSON.stringify(input, null, 2));
|
|
152
|
+
const [result, setResult] = useState<unknown>(null);
|
|
153
|
+
const [running, setRunning] = useState(false);
|
|
154
|
+
const [inputError, setInputError] = useState<string | null>(null);
|
|
155
|
+
|
|
156
|
+
const runnable = tool.host === 'node';
|
|
157
|
+
const execute = async () => {
|
|
158
|
+
const confirm =
|
|
159
|
+
tool.permission.risk === 'read' ||
|
|
160
|
+
window.confirm(`${tool.permission.summary}\n\nRun ${tool.name}?`);
|
|
161
|
+
if (!confirm) return;
|
|
162
|
+
setRunning(true);
|
|
163
|
+
try {
|
|
164
|
+
setResult(await runProjectTool(tool.name, input, { confirm: true }));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
setResult({ ok: false, error: { code: 'REQUEST_FAILED', message: String(error) } });
|
|
167
|
+
} finally {
|
|
168
|
+
setRunning(false);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div style={rootStyle} data-testid={`project-tool:${tool.name}`}>
|
|
174
|
+
<h2 style={headingStyle}>{tool.summary}</h2>
|
|
175
|
+
<code style={{ color: text[2] }}>{tool.name}</code>
|
|
176
|
+
<p style={{ color: text[2] }}>{tool.description}</p>
|
|
177
|
+
<div style={mutedStyle}>
|
|
178
|
+
{tool.host} · {tool.permission.risk} · {tool.sourcePath}
|
|
179
|
+
</div>
|
|
180
|
+
{!runnable && (
|
|
181
|
+
<div style={errorStyle}>
|
|
182
|
+
This runner supports Node-hosted tools. Import the underlying function directly in browser
|
|
183
|
+
or runtime code.
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
<section style={{ marginTop: 18, maxWidth: 760 }}>
|
|
187
|
+
<ToolSchemaForm
|
|
188
|
+
schema={tool.inputSchema}
|
|
189
|
+
value={input}
|
|
190
|
+
onChange={(next) => {
|
|
191
|
+
setInput(next);
|
|
192
|
+
setRawText(JSON.stringify(next, null, 2));
|
|
193
|
+
setInputError(null);
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
</section>
|
|
197
|
+
{inputError && <div style={errorStyle}>{inputError}</div>}
|
|
198
|
+
<Button
|
|
199
|
+
variant="primary"
|
|
200
|
+
disabled={!runnable || running || inputError !== null}
|
|
201
|
+
aria-busy={running}
|
|
202
|
+
onClick={() => void execute()}
|
|
203
|
+
style={{ marginTop: 16 }}
|
|
204
|
+
>
|
|
205
|
+
{running ? 'Running…' : 'Run tool'}
|
|
206
|
+
</Button>
|
|
207
|
+
<details style={{ marginTop: 16 }}>
|
|
208
|
+
<summary style={labelStyle}>Advanced JSON</summary>
|
|
209
|
+
<textarea
|
|
210
|
+
className="vgai-input"
|
|
211
|
+
id={`tool-input:${tool.name}`}
|
|
212
|
+
value={rawText}
|
|
213
|
+
onChange={(event) => {
|
|
214
|
+
const nextText = event.target.value;
|
|
215
|
+
setRawText(nextText);
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(nextText) as unknown;
|
|
218
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
219
|
+
throw new Error('Input must be a JSON object.');
|
|
220
|
+
}
|
|
221
|
+
setInput(parsed as Record<string, unknown>);
|
|
222
|
+
setInputError(null);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
setInputError(error instanceof Error ? error.message : String(error));
|
|
225
|
+
}
|
|
226
|
+
}}
|
|
227
|
+
rows={10}
|
|
228
|
+
style={textareaStyle}
|
|
229
|
+
/>
|
|
230
|
+
<details>
|
|
231
|
+
<summary style={labelStyle}>Input schema</summary>
|
|
232
|
+
<JsonBlock value={tool.inputSchema} />
|
|
233
|
+
</details>
|
|
234
|
+
</details>
|
|
235
|
+
{result !== null && (
|
|
236
|
+
<section>
|
|
237
|
+
<div style={labelStyle}>Result</div>
|
|
238
|
+
<JsonBlock value={result} />
|
|
239
|
+
</section>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const rootStyle: React.CSSProperties = {
|
|
246
|
+
width: '100%',
|
|
247
|
+
maxWidth: 960,
|
|
248
|
+
margin: '0 auto',
|
|
249
|
+
padding: 24,
|
|
250
|
+
boxSizing: 'border-box',
|
|
251
|
+
overflow: 'auto',
|
|
252
|
+
pointerEvents: 'auto',
|
|
253
|
+
color: text[1],
|
|
254
|
+
fontSize: fontSize.base,
|
|
255
|
+
};
|
|
256
|
+
const headingStyle: React.CSSProperties = { margin: '0 0 6px', fontSize: 20 };
|
|
257
|
+
const mutedStyle: React.CSSProperties = { color: text[2], fontSize: fontSize.sm };
|
|
258
|
+
const labelStyle: React.CSSProperties = { display: 'block', margin: '14px 0 6px', color: text[1] };
|
|
259
|
+
const cardTitleRowStyle: React.CSSProperties = {
|
|
260
|
+
display: 'flex',
|
|
261
|
+
width: '100%',
|
|
262
|
+
alignItems: 'center',
|
|
263
|
+
justifyContent: 'space-between',
|
|
264
|
+
gap: 12,
|
|
265
|
+
};
|
|
266
|
+
const openLabelStyle: React.CSSProperties = {
|
|
267
|
+
color: text[2],
|
|
268
|
+
fontSize: fontSize.sm,
|
|
269
|
+
fontWeight: 500,
|
|
270
|
+
};
|
|
271
|
+
const cardMetaRowStyle: React.CSSProperties = {
|
|
272
|
+
display: 'flex',
|
|
273
|
+
width: '100%',
|
|
274
|
+
minWidth: 0,
|
|
275
|
+
alignItems: 'center',
|
|
276
|
+
justifyContent: 'space-between',
|
|
277
|
+
gap: 12,
|
|
278
|
+
color: text[2],
|
|
279
|
+
fontSize: fontSize.sm,
|
|
280
|
+
};
|
|
281
|
+
const badgeStyle: React.CSSProperties = {
|
|
282
|
+
flex: '0 0 auto',
|
|
283
|
+
};
|
|
284
|
+
const errorStyle: React.CSSProperties = { marginTop: 10, color: danger, whiteSpace: 'pre-wrap' };
|
|
285
|
+
const textareaStyle: React.CSSProperties = {
|
|
286
|
+
width: '100%',
|
|
287
|
+
boxSizing: 'border-box',
|
|
288
|
+
resize: 'vertical',
|
|
289
|
+
fontFamily: 'monospace',
|
|
290
|
+
};
|
|
291
|
+
const jsonStyle: React.CSSProperties = {
|
|
292
|
+
overflow: 'auto',
|
|
293
|
+
padding: 10,
|
|
294
|
+
background: bg.inset,
|
|
295
|
+
border: `1px solid ${border[1]}`,
|
|
296
|
+
borderRadius: radius.sm,
|
|
297
|
+
color: text[2],
|
|
298
|
+
};
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
import type { ConnectionState } from '@engine/adapter';
|
|
27
27
|
import { useSyncExternalStore } from 'react';
|
|
28
|
+
import {
|
|
29
|
+
assetImportJobs,
|
|
30
|
+
assetImportJobsVersion,
|
|
31
|
+
subscribeAssetImportJobs,
|
|
32
|
+
} from '../asset-workflow/asset-import-jobs';
|
|
28
33
|
import { getActiveNetworking } from '../authoring/active-systems';
|
|
29
34
|
import { useAvailabilityTick } from '../availability-tick';
|
|
30
35
|
import { Button, themeVars } from '../design-system';
|
|
@@ -100,6 +105,31 @@ export function ConnectionStatus() {
|
|
|
100
105
|
);
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
/** Project-wide import status remains visible when the Library document closes. */
|
|
109
|
+
export function AssetImportStatus() {
|
|
110
|
+
useSyncExternalStore(subscribeAssetImportJobs, assetImportJobsVersion);
|
|
111
|
+
const jobs = assetImportJobs();
|
|
112
|
+
if (jobs.length === 0) return null;
|
|
113
|
+
const running = jobs.filter((job) => job.status === 'running').length;
|
|
114
|
+
const failed = jobs.filter((job) => job.status === 'failed').length;
|
|
115
|
+
const completed = jobs.filter(
|
|
116
|
+
(job) => job.status === 'imported' || job.status === 'skipped',
|
|
117
|
+
).length;
|
|
118
|
+
const label = running
|
|
119
|
+
? `${running} importing`
|
|
120
|
+
: failed
|
|
121
|
+
? `${failed} import failed`
|
|
122
|
+
: `${completed} imported`;
|
|
123
|
+
return (
|
|
124
|
+
<span
|
|
125
|
+
data-testid="status-asset-imports"
|
|
126
|
+
style={{ color: failed ? themeVars.semantic.danger : themeVars.content.muted }}
|
|
127
|
+
>
|
|
128
|
+
{label}
|
|
129
|
+
</span>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
103
133
|
/** Right-aligned concise selection summary (§4.1 "Board selected"). */
|
|
104
134
|
export function SelectionStatus() {
|
|
105
135
|
const store = useEditorStore();
|
|
@@ -155,6 +185,14 @@ export function ensureCoreStatusContributionsRegistered(): void {
|
|
|
155
185
|
Content: ConnectionStatus,
|
|
156
186
|
}),
|
|
157
187
|
);
|
|
188
|
+
track(
|
|
189
|
+
registerWorkspaceStatus({
|
|
190
|
+
id: 'asset-imports',
|
|
191
|
+
align: 'left',
|
|
192
|
+
order: 40,
|
|
193
|
+
Content: AssetImportStatus,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
158
196
|
// `build-progress` (build-documents.tsx, server mode) registers with
|
|
159
197
|
// order 0/default and a later sequence, so it lands after `save-status`
|
|
160
198
|
// among the order-0 items — visually beside the session state it extends.
|
|
@@ -24,7 +24,11 @@ import { themeVars } from '../design-system';
|
|
|
24
24
|
|
|
25
25
|
import { useSyncExternalStore } from 'react';
|
|
26
26
|
import type { EditorAction } from '../action-registry';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
getDocumentToolContributions,
|
|
29
|
+
getGlobalToolContributions,
|
|
30
|
+
subscribeToolContributions,
|
|
31
|
+
} from '../tool-loader';
|
|
28
32
|
import {
|
|
29
33
|
openWorkspaceDocument,
|
|
30
34
|
type WorkspaceDocumentContentProps,
|
|
@@ -46,18 +50,18 @@ export function toolDocumentId(toolId: string): string {
|
|
|
46
50
|
* tool. Returns `false` (opens nothing) when no discovered document tool
|
|
47
51
|
* carries that id — no fabricated documents for deleted/renamed tools.
|
|
48
52
|
*/
|
|
49
|
-
export function openToolDocument(store: ToolDocumentStore, toolId: string): boolean {
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
53
|
+
export function openToolDocument(store: ToolDocumentStore | null, toolId: string): boolean {
|
|
54
|
+
const contribution = getDocumentToolContributions().find((item) => item.id === toolId);
|
|
55
|
+
if (!contribution) return false;
|
|
52
56
|
openWorkspaceDocument({
|
|
53
57
|
id: toolDocumentId(toolId),
|
|
54
58
|
// §8: the tool's own declared title (`Tuning`), never a tech bucket.
|
|
55
|
-
title:
|
|
56
|
-
kind: 'tool',
|
|
57
|
-
provenance: { sourcePath:
|
|
59
|
+
title: contribution.title,
|
|
60
|
+
kind: 'tool-contribution',
|
|
61
|
+
provenance: { sourcePath: contribution.file, origin: contribution.tool.name },
|
|
58
62
|
Content: ToolDocumentContent,
|
|
59
63
|
closeable: true,
|
|
60
|
-
onActivate: () => store.setActiveViewportTab('scene'),
|
|
64
|
+
...(store ? { onActivate: () => store.setActiveViewportTab('scene') } : {}),
|
|
61
65
|
});
|
|
62
66
|
return true;
|
|
63
67
|
}
|
|
@@ -65,7 +69,7 @@ export function openToolDocument(store: ToolDocumentStore, toolId: string): bool
|
|
|
65
69
|
/** `Open Tool: <title>` palette actions — one per discovered document tool
|
|
66
70
|
* (the B12 discovery gesture). */
|
|
67
71
|
export function buildToolActions(store: ToolDocumentStore): EditorAction[] {
|
|
68
|
-
return
|
|
72
|
+
return getDocumentToolContributions().map((t) => ({
|
|
69
73
|
id: `tool-doc:${t.id}`,
|
|
70
74
|
label: `Open Tool: ${t.title}`,
|
|
71
75
|
category: 'action' as const,
|
|
@@ -76,23 +80,22 @@ export function buildToolActions(store: ToolDocumentStore): EditorAction[] {
|
|
|
76
80
|
/** Content host: resolves the LIVE tool by id on every render (HMR re-key,
|
|
77
81
|
* honest deleted-tool state). */
|
|
78
82
|
export function ToolDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
|
|
79
|
-
useSyncExternalStore(
|
|
83
|
+
useSyncExternalStore(subscribeToolContributions, getGlobalToolContributions);
|
|
80
84
|
const toolId = documentId.startsWith('tool:') ? documentId.slice('tool:'.length) : documentId;
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
85
|
+
const contribution = getDocumentToolContributions().find((item) => item.id === toolId);
|
|
86
|
+
if (!contribution) {
|
|
83
87
|
return (
|
|
84
88
|
<div
|
|
85
89
|
style={{ padding: 12, fontSize: 12, color: themeVars.content.muted, pointerEvents: 'auto' }}
|
|
86
90
|
>
|
|
87
|
-
This tool is no longer
|
|
88
|
-
document.
|
|
91
|
+
This tool contribution is no longer registered — close this document.
|
|
89
92
|
</div>
|
|
90
93
|
);
|
|
91
94
|
}
|
|
92
95
|
return (
|
|
93
96
|
<div
|
|
94
|
-
key={`${
|
|
95
|
-
data-testid={`tool-document:${
|
|
97
|
+
key={`${contribution.id}:${contribution.version}`}
|
|
98
|
+
data-testid={`tool-document:${contribution.id}`}
|
|
96
99
|
style={{
|
|
97
100
|
flex: 1,
|
|
98
101
|
minHeight: 0,
|
|
@@ -104,7 +107,7 @@ export function ToolDocumentContent({ documentId }: WorkspaceDocumentContentProp
|
|
|
104
107
|
padding: 12,
|
|
105
108
|
}}
|
|
106
109
|
>
|
|
107
|
-
<ToolHost
|
|
110
|
+
<ToolHost contribution={contribution} />
|
|
108
111
|
</div>
|
|
109
112
|
);
|
|
110
113
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import { Checkbox, fontSize, Select, TextInput, text } from '../design-system';
|
|
3
|
+
|
|
4
|
+
type JsonSchema = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function schemaRecord(value: unknown): JsonSchema {
|
|
7
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonSchema) : {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function schemaType(schema: JsonSchema): string | undefined {
|
|
11
|
+
const type = schema['type'];
|
|
12
|
+
if (typeof type === 'string') return type;
|
|
13
|
+
if (Array.isArray(type)) return type.find((item): item is string => item !== 'null');
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function defaultValueForSchema(value: unknown): unknown {
|
|
18
|
+
const schema = schemaRecord(value);
|
|
19
|
+
if ('default' in schema) return structuredClone(schema['default']);
|
|
20
|
+
const type = schemaType(schema);
|
|
21
|
+
if (type === 'object' || schema['properties']) {
|
|
22
|
+
const result: Record<string, unknown> = {};
|
|
23
|
+
for (const [key, child] of Object.entries(schemaRecord(schema['properties']))) {
|
|
24
|
+
const childSchema = schemaRecord(child);
|
|
25
|
+
if (
|
|
26
|
+
'default' in childSchema ||
|
|
27
|
+
(Array.isArray(schema['required']) && schema['required'].includes(key))
|
|
28
|
+
) {
|
|
29
|
+
result[key] = defaultValueForSchema(childSchema);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
if (type === 'array') return [];
|
|
35
|
+
if (type === 'boolean') return false;
|
|
36
|
+
if (type === 'number' || type === 'integer') return Number(schema['minimum'] ?? 0);
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function humanize(value: string): string {
|
|
41
|
+
return value
|
|
42
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
43
|
+
.replace(/[-_]/g, ' ')
|
|
44
|
+
.replace(/^./, (letter) => letter.toUpperCase());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: this is the deliberately small JSON-Schema widget dispatch; each primitive type remains native HTML.
|
|
48
|
+
function PrimitiveField({
|
|
49
|
+
name,
|
|
50
|
+
schema,
|
|
51
|
+
value,
|
|
52
|
+
required,
|
|
53
|
+
onChange,
|
|
54
|
+
}: {
|
|
55
|
+
name: string;
|
|
56
|
+
schema: JsonSchema;
|
|
57
|
+
value: unknown;
|
|
58
|
+
required: boolean;
|
|
59
|
+
onChange(value: unknown): void;
|
|
60
|
+
}) {
|
|
61
|
+
const type = schemaType(schema);
|
|
62
|
+
const description = typeof schema['description'] === 'string' ? schema['description'] : null;
|
|
63
|
+
const choices = Array.isArray(schema['enum']) ? schema['enum'] : null;
|
|
64
|
+
const id = `tool-field:${name}`;
|
|
65
|
+
|
|
66
|
+
if (type === 'boolean') {
|
|
67
|
+
return (
|
|
68
|
+
<label style={{ ...fieldStyle, gridTemplateColumns: '18px minmax(0, 1fr)' }}>
|
|
69
|
+
<Checkbox
|
|
70
|
+
id={id}
|
|
71
|
+
checked={value === true}
|
|
72
|
+
onChange={(event) => onChange(event.target.checked)}
|
|
73
|
+
/>
|
|
74
|
+
<span>
|
|
75
|
+
<span style={fieldLabelStyle}>{humanize(name)}</span>
|
|
76
|
+
{description && <span style={descriptionStyle}>{description}</span>}
|
|
77
|
+
</span>
|
|
78
|
+
</label>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const label = (
|
|
83
|
+
<span style={fieldLabelStyle}>
|
|
84
|
+
{humanize(name)} {!required && <span style={optionalStyle}>optional</span>}
|
|
85
|
+
</span>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (choices) {
|
|
89
|
+
return (
|
|
90
|
+
<label style={fieldStyle} htmlFor={id}>
|
|
91
|
+
{label}
|
|
92
|
+
<Select
|
|
93
|
+
id={id}
|
|
94
|
+
value={String(value ?? '')}
|
|
95
|
+
onChange={(event) => onChange(event.target.value)}
|
|
96
|
+
style={controlLayoutStyle}
|
|
97
|
+
>
|
|
98
|
+
{!required && <option value="">Not set</option>}
|
|
99
|
+
{choices.map((choice) => (
|
|
100
|
+
<option key={String(choice)} value={String(choice)}>
|
|
101
|
+
{String(choice)}
|
|
102
|
+
</option>
|
|
103
|
+
))}
|
|
104
|
+
</Select>
|
|
105
|
+
{description && <span style={descriptionStyle}>{description}</span>}
|
|
106
|
+
</label>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (type === 'number' || type === 'integer') {
|
|
111
|
+
return (
|
|
112
|
+
<label style={fieldStyle} htmlFor={id}>
|
|
113
|
+
{label}
|
|
114
|
+
<TextInput
|
|
115
|
+
id={id}
|
|
116
|
+
type="number"
|
|
117
|
+
value={typeof value === 'number' ? value : ''}
|
|
118
|
+
min={typeof schema['minimum'] === 'number' ? schema['minimum'] : undefined}
|
|
119
|
+
max={typeof schema['maximum'] === 'number' ? schema['maximum'] : undefined}
|
|
120
|
+
step={type === 'integer' ? 1 : 'any'}
|
|
121
|
+
onChange={(event) =>
|
|
122
|
+
onChange(event.target.value === '' ? undefined : Number(event.target.value))
|
|
123
|
+
}
|
|
124
|
+
style={controlLayoutStyle}
|
|
125
|
+
/>
|
|
126
|
+
{description && <span style={descriptionStyle}>{description}</span>}
|
|
127
|
+
</label>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (type === 'array' && schemaType(schemaRecord(schema['items'])) === 'string') {
|
|
132
|
+
return (
|
|
133
|
+
<label style={fieldStyle} htmlFor={id}>
|
|
134
|
+
{label}
|
|
135
|
+
<TextInput
|
|
136
|
+
id={id}
|
|
137
|
+
value={Array.isArray(value) ? value.join(', ') : ''}
|
|
138
|
+
onChange={(event) =>
|
|
139
|
+
onChange(
|
|
140
|
+
event.target.value
|
|
141
|
+
.split(',')
|
|
142
|
+
.map((item) => item.trim())
|
|
143
|
+
.filter(Boolean),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
placeholder="Comma-separated values"
|
|
147
|
+
style={controlLayoutStyle}
|
|
148
|
+
/>
|
|
149
|
+
{description && <span style={descriptionStyle}>{description}</span>}
|
|
150
|
+
</label>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<label style={fieldStyle} htmlFor={id}>
|
|
156
|
+
{label}
|
|
157
|
+
<TextInput
|
|
158
|
+
id={id}
|
|
159
|
+
value={typeof value === 'string' ? value : ''}
|
|
160
|
+
onChange={(event) => onChange(event.target.value || (required ? '' : undefined))}
|
|
161
|
+
style={controlLayoutStyle}
|
|
162
|
+
/>
|
|
163
|
+
{description && <span style={descriptionStyle}>{description}</span>}
|
|
164
|
+
</label>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function ObjectFields({
|
|
169
|
+
schema,
|
|
170
|
+
value,
|
|
171
|
+
onChange,
|
|
172
|
+
path = '',
|
|
173
|
+
}: {
|
|
174
|
+
schema: JsonSchema;
|
|
175
|
+
value: Record<string, unknown>;
|
|
176
|
+
onChange(value: Record<string, unknown>): void;
|
|
177
|
+
path?: string;
|
|
178
|
+
}) {
|
|
179
|
+
const properties = schemaRecord(schema['properties']);
|
|
180
|
+
const required = new Set(Array.isArray(schema['required']) ? schema['required'] : []);
|
|
181
|
+
return (
|
|
182
|
+
<div style={{ display: 'grid', gap: 14 }}>
|
|
183
|
+
{Object.entries(properties).map(([key, child]) => {
|
|
184
|
+
const childSchema = schemaRecord(child);
|
|
185
|
+
const childType = schemaType(childSchema);
|
|
186
|
+
const update = (next: unknown) => {
|
|
187
|
+
const copy = { ...value };
|
|
188
|
+
if (next === undefined) delete copy[key];
|
|
189
|
+
else copy[key] = next;
|
|
190
|
+
onChange(copy);
|
|
191
|
+
};
|
|
192
|
+
if (childType === 'object' || childSchema['properties']) {
|
|
193
|
+
return (
|
|
194
|
+
<fieldset key={key} style={fieldsetStyle}>
|
|
195
|
+
<legend style={legendStyle}>{humanize(key)}</legend>
|
|
196
|
+
{typeof childSchema['description'] === 'string' && (
|
|
197
|
+
<p style={{ ...descriptionStyle, marginTop: 0 }}>{childSchema['description']}</p>
|
|
198
|
+
)}
|
|
199
|
+
<ObjectFields
|
|
200
|
+
schema={childSchema}
|
|
201
|
+
value={schemaRecord(value[key])}
|
|
202
|
+
onChange={update}
|
|
203
|
+
path={path ? `${path}.${key}` : key}
|
|
204
|
+
/>
|
|
205
|
+
</fieldset>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return (
|
|
209
|
+
<PrimitiveField
|
|
210
|
+
key={key}
|
|
211
|
+
name={path ? `${path}.${key}` : key}
|
|
212
|
+
schema={childSchema}
|
|
213
|
+
value={value[key]}
|
|
214
|
+
required={required.has(key)}
|
|
215
|
+
onChange={update}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function ToolSchemaForm({
|
|
224
|
+
schema: schemaValue,
|
|
225
|
+
value,
|
|
226
|
+
onChange,
|
|
227
|
+
}: {
|
|
228
|
+
schema: unknown;
|
|
229
|
+
value: Record<string, unknown>;
|
|
230
|
+
onChange(value: Record<string, unknown>): void;
|
|
231
|
+
}) {
|
|
232
|
+
const schema = schemaRecord(schemaValue);
|
|
233
|
+
if (!schema['properties']) {
|
|
234
|
+
return <div style={descriptionStyle}>This schema has no form-compatible fields.</div>;
|
|
235
|
+
}
|
|
236
|
+
return <ObjectFields schema={schema} value={value} onChange={onChange} />;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const fieldStyle: CSSProperties = { display: 'grid', gap: 5 };
|
|
240
|
+
const fieldLabelStyle: CSSProperties = { color: text[1], fontWeight: 600, fontSize: fontSize.sm };
|
|
241
|
+
const optionalStyle: CSSProperties = { color: text[3], fontWeight: 400, marginLeft: 5 };
|
|
242
|
+
const descriptionStyle: CSSProperties = {
|
|
243
|
+
display: 'block',
|
|
244
|
+
color: text[2],
|
|
245
|
+
fontSize: fontSize.sm,
|
|
246
|
+
lineHeight: 1.4,
|
|
247
|
+
};
|
|
248
|
+
const controlLayoutStyle: CSSProperties = {
|
|
249
|
+
boxSizing: 'border-box',
|
|
250
|
+
width: '100%',
|
|
251
|
+
};
|
|
252
|
+
const fieldsetStyle: CSSProperties = {
|
|
253
|
+
margin: 0,
|
|
254
|
+
padding: 14,
|
|
255
|
+
border: '1px solid var(--vgai-boundary-default)',
|
|
256
|
+
borderRadius: 'var(--vgai-radius-md)',
|
|
257
|
+
};
|
|
258
|
+
const legendStyle: CSSProperties = { padding: '0 6px', color: text[1], fontWeight: 600 };
|