@volter/editor-sdk 0.5.57

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 (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +19 -0
  4. package/package.json +90 -0
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +83 -0
  7. package/src/client.ts +1547 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +985 -0
  10. package/src/document-probe.ts +237 -0
  11. package/src/editor-view.ts +220 -0
  12. package/src/extension.ts +40 -0
  13. package/src/generations.ts +178 -0
  14. package/src/host.ts +1167 -0
  15. package/src/http-transport.browser.ts +14 -0
  16. package/src/http-transport.node.ts +19 -0
  17. package/src/index.ts +128 -0
  18. package/src/layout-arrangements.ts +5 -0
  19. package/src/layouts.tsx +108 -0
  20. package/src/looks.ts +14 -0
  21. package/src/project/output-roots.ts +73 -0
  22. package/src/project/tab-census.ts +149 -0
  23. package/src/project-tool-catalog.ts +96 -0
  24. package/src/selection.tsx +108 -0
  25. package/src/services.ts +18 -0
  26. package/src/session/build-report.ts +19 -0
  27. package/src/session/collaboration-types.ts +262 -0
  28. package/src/session/command-table.ts +333 -0
  29. package/src/session/discovery.ts +90 -0
  30. package/src/session/editor-brand.ts +73 -0
  31. package/src/session/editor-compatibility.ts +248 -0
  32. package/src/session/editor-control-lifecycle.ts +68 -0
  33. package/src/session/editor-control-protocol.ts +5 -0
  34. package/src/session/entrypoint-selection-readers.ts +66 -0
  35. package/src/session/entrypoint-selection-source.ts +120 -0
  36. package/src/session/game-css-scope.ts +30 -0
  37. package/src/session/product-create.ts +24 -0
  38. package/src/session/product-locator.ts +389 -0
  39. package/src/session/project-module-url.ts +245 -0
  40. package/src/session/registry-format.ts +203 -0
  41. package/src/session/relative-path-guard.ts +56 -0
  42. package/src/session/source-glob.ts +15 -0
  43. package/src/session/tool-contribution-convention.ts +116 -0
  44. package/src/session/workbench-locator.ts +650 -0
  45. package/src/session.ts +41 -0
  46. package/src/share.ts +160 -0
  47. package/src/tools/errors.ts +91 -0
  48. package/src/tools/provider-execution.ts +70 -0
  49. package/src/tools/registry.ts +341 -0
  50. package/src/tools/types.ts +159 -0
  51. package/src/transport.ts +97 -0
  52. package/src/types.ts +1581 -0
  53. package/src/views.ts +164 -0
  54. package/src/widgets/design-system.ts +93 -0
  55. package/src/widgets/editor-appearance.ts +149 -0
  56. package/src/widgets/editor-material.ts +83 -0
  57. package/src/widgets/icon-set-registry.ts +105 -0
  58. package/src/widgets/index.ts +71 -0
  59. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  60. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  61. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  62. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  63. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  64. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  65. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  66. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  67. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  68. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  69. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  70. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  71. package/src/widgets/inspector-widgets/index.ts +24 -0
  72. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  73. package/src/widgets/interactive-edit-scope.ts +33 -0
  74. package/src/widgets/patterns/Dialog.tsx +129 -0
  75. package/src/widgets/patterns/Fields.tsx +44 -0
  76. package/src/widgets/patterns/List.tsx +25 -0
  77. package/src/widgets/patterns/StateSurface.tsx +40 -0
  78. package/src/widgets/patterns/Surfaces.tsx +122 -0
  79. package/src/widgets/patterns/Tabs.tsx +80 -0
  80. package/src/widgets/patterns/Toolbar.tsx +72 -0
  81. package/src/widgets/patterns/Tree.tsx +72 -0
  82. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  83. package/src/widgets/primitives/Button.tsx +62 -0
  84. package/src/widgets/primitives/ColorInput.tsx +78 -0
  85. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  86. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  87. package/src/widgets/primitives/FormControls.tsx +88 -0
  88. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  89. package/src/widgets/primitives/JsonInput.tsx +113 -0
  90. package/src/widgets/primitives/Layout.tsx +100 -0
  91. package/src/widgets/primitives/Menu.tsx +140 -0
  92. package/src/widgets/primitives/NumberInput.tsx +169 -0
  93. package/src/widgets/primitives/Panel.tsx +80 -0
  94. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  95. package/src/widgets/primitives/Text.tsx +54 -0
  96. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  97. package/src/widgets/primitives/Tooltip.tsx +204 -0
  98. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  99. package/src/widgets/primitives/banner-tones.ts +32 -0
  100. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  101. package/src/widgets/primitives/editor-icons.ts +245 -0
  102. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  103. package/src/widgets/theme.ts +2633 -0
  104. package/src/widgets/z-index.ts +25 -0
@@ -0,0 +1,14 @@
1
+ /** Browser fetch owns connections and has no Node header timeout. */
2
+ import type { Dispatcher } from 'undici';
3
+
4
+ export function createDispatcher(_timeoutMs: number): undefined {
5
+ return undefined;
6
+ }
7
+
8
+ export function dispatchFetch(
9
+ url: string,
10
+ init?: RequestInit,
11
+ _dispatcher?: Dispatcher,
12
+ ): Promise<Response> {
13
+ return fetch(url, init);
14
+ }
@@ -0,0 +1,19 @@
1
+ /** Node session clients need explicit header deadlines for long modeling calls.
2
+ * Some runtimes supply an undici-compatible Agent without destroy(); callers
3
+ * close explicitly when that lifecycle method exists. */
4
+ import { Agent, fetch as nodeFetch, type Dispatcher } from 'undici';
5
+
6
+ export function createDispatcher(timeoutMs: number): Dispatcher {
7
+ return new Agent({ headersTimeout: timeoutMs, bodyTimeout: timeoutMs });
8
+ }
9
+
10
+ export async function dispatchFetch(
11
+ url: string,
12
+ init?: RequestInit,
13
+ dispatcher?: Dispatcher,
14
+ ): Promise<Response> {
15
+ if (!dispatcher) return fetch(url, init);
16
+ return await nodeFetch(url, {
17
+ ...(init as object), dispatcher,
18
+ }) as unknown as Response;
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,128 @@
1
+ export type {
2
+ GenerationBilling,
3
+ GenerationJob,
4
+ GenerationJobStatus,
5
+ GenerationJobsDocument,
6
+ } from '@volter/editor-sdk/generations';
7
+ export type {
8
+ EditorEnvelopeObservation,
9
+ GameDebugDoor,
10
+ HistoryStep,
11
+ OpenedDocument,
12
+ } from './client.js';
13
+ export { EditorClient, EditorCommandError } from './client.js';
14
+ export type {
15
+ ToolAssetInspectorContributionMatch,
16
+ ToolAssetInspectorContributionProps,
17
+ ToolContributionAsset,
18
+ ToolContributionNode,
19
+ ToolContributionProps,
20
+ ToolGenerationResultContributionProps,
21
+ ToolInspectorContributionMatch,
22
+ ToolInspectorContributionProps,
23
+ } from './contributions.js';
24
+ export type {
25
+ DocumentClickStep,
26
+ DocumentKeyStep,
27
+ DocumentPasteStep,
28
+ DocumentProbeResult,
29
+ DocumentProbeScope,
30
+ DocumentProbeStep,
31
+ DocumentQueryStep,
32
+ DocumentSelectStep,
33
+ DocumentTypeStep,
34
+ ProbedElement,
35
+ } from './document-probe.js';
36
+ export { editorViewFromUrl, editorViewUrl } from './editor-view.js';
37
+ export type { ExtensionContributionState } from './extension.js';
38
+ export type {
39
+ ActiveDocumentCapture,
40
+ AssetCompareCapture,
41
+ AssetCompareOptions,
42
+ AssetCompareView,
43
+ AssetKind,
44
+ AssetPreviewBackground,
45
+ AssetPreviewCameraChoice,
46
+ AssetPreviewCapture,
47
+ AssetPreviewOptions,
48
+ AssetPreviewOrientation,
49
+ AssetPreviewPose,
50
+ AssetPreviewShotSetDefinition,
51
+ AssetPreviewShotWarning,
52
+ AssetPreviewSource,
53
+ AssetPreviewStage,
54
+ AssetPreviewView,
55
+ CaptureDimensions,
56
+ DocumentCameraPose,
57
+ DocumentLookOutcome,
58
+ DocumentTableEntryProjection,
59
+ DocumentTableProjection,
60
+ EditorCameraState,
61
+ EditorChromeCapture,
62
+ EditorChromeCaptureOptions,
63
+ EditorEntitySummary,
64
+ EditorState,
65
+ EditorView,
66
+ EditorViewBuiltInUtilityId,
67
+ EditorViewDocument,
68
+ EditorViewUtility,
69
+ EditorWorkspaceName,
70
+ GameCapture,
71
+ GameplayRecordingCapture,
72
+ GameplayRecordingOptions,
73
+ GameplayRecordingStarted,
74
+ GameplayRecordingTimeline,
75
+ GameplayReplayCapture,
76
+ HelperVisibility,
77
+ InspectedAction,
78
+ InspectedField,
79
+ InspectedFieldWrite,
80
+ InspectedHierarchy,
81
+ InspectedHierarchyRow,
82
+ InspectedInspection,
83
+ InspectedNothing,
84
+ InspectedSection,
85
+ InspectedSectionBody,
86
+ InspectedSubject,
87
+ InspectedWriteDestination,
88
+ InspectionPresentationKind,
89
+ InspectionSurface,
90
+ LabeledShotSetCapture,
91
+ PlayRecordingStatus,
92
+ PlayStarted,
93
+ PresentedEditorView,
94
+ ProjectInfo,
95
+ ProjectTemplate,
96
+ ProjectToolCatalog,
97
+ ProjectToolCatalogEntry,
98
+ ProjectToolContribution,
99
+ ProjectToolOutcome,
100
+ RecentProject,
101
+ ShadingMode,
102
+ ShotSetPoseMorph,
103
+ ShotSetPoseRotation,
104
+ ShotSetPoseStep,
105
+ ShotSetPoseTranslation,
106
+ ShotSetShot,
107
+ StoryCaptureOptions,
108
+ StoryVariantCapture,
109
+ StoryVariantImage,
110
+ StructureOp,
111
+ StructureOpOptions,
112
+ StructureOpResult,
113
+ TransformMode,
114
+ TransformSpace,
115
+ Vec3Value,
116
+ ViewPreset,
117
+ ViewportCapture,
118
+ ViewportTab,
119
+ } from './types.js';
120
+ export {
121
+ EDITOR_VIEW_BUILT_IN_UTILITY_IDS,
122
+ EDITOR_VIEW_DOCUMENT_KINDS,
123
+ EDITOR_VIEW_KEYS,
124
+ EDITOR_VIEW_TOOL_UTILITY_PREFIX,
125
+ EDITOR_VIEW_WORKSPACE_DOCUMENT_IDS,
126
+ isEditorViewDocumentKind,
127
+ isEditorViewUtility,
128
+ } from './types.js';
@@ -0,0 +1,5 @@
1
+ import type { WorkspaceArrangement } from '@volter/editor-project/adapter/workspace-arrangement';
2
+ export type { WorkspaceArrangement, WorkspaceAreaContribution } from '@volter/editor-project/adapter/workspace-arrangement';
3
+
4
+ export const GameArrangement: WorkspaceArrangement = { id: 'game', title: 'Game' };
5
+ export const DesignArrangement: WorkspaceArrangement = { id: 'design', title: 'Design' };
@@ -0,0 +1,108 @@
1
+ /** Programmable editor composition. Layouts are React components, not registry keys. */
2
+ import { type ComponentType, type CSSProperties, createElement, type ReactNode } from 'react';
3
+ import { DesignArrangement, type WorkspaceArrangement } from './layout-arrangements';
4
+ import type { EditorView } from './types';
5
+
6
+ export {
7
+ DesignArrangement,
8
+ GameArrangement,
9
+ type WorkspaceArrangement,
10
+ } from './layout-arrangements';
11
+
12
+ export interface EditorLayoutProps {
13
+ readonly playing: boolean;
14
+ readonly paused: boolean;
15
+ }
16
+ export type EditorLayout = ComponentType<EditorLayoutProps>;
17
+ export interface LayoutFrameProps {
18
+ readonly children?: ReactNode;
19
+ readonly style?: CSSProperties;
20
+ /** Let the enclosing web page scroll when the pointer is over a viewport. */
21
+ readonly pageScroll?: boolean;
22
+ }
23
+ export interface WorkspaceProps {
24
+ readonly arrangement?: WorkspaceArrangement;
25
+ readonly immersivePlay?: boolean;
26
+ readonly playUtilities?: readonly string[];
27
+ }
28
+ export interface DocumentHandle {
29
+ /** Select exactly one named native object; ambiguous/missing names refuse. */
30
+ select(name: string): void;
31
+ transform(mode: 'combined' | 'translate' | 'rotate' | 'scale'): void;
32
+ frame(): void;
33
+ }
34
+ export interface DocumentViewProps {
35
+ readonly document:
36
+ | Extract<NonNullable<EditorView['document']>, { kind: 'story' | 'workspace' }>
37
+ | {
38
+ readonly kind: 'asset';
39
+ readonly path: string;
40
+ readonly assetKind?: 'source' | 'model' | 'image' | 'audio' | 'json';
41
+ };
42
+ readonly chrome?: boolean;
43
+ readonly active?: boolean;
44
+ readonly onReady?: (document: DocumentHandle) => void;
45
+ }
46
+ export interface LayoutHost {
47
+ readonly Frame: ComponentType<LayoutFrameProps>;
48
+ readonly Header: ComponentType;
49
+ readonly Footer: ComponentType;
50
+ readonly Workspace: ComponentType<WorkspaceProps>;
51
+ readonly Document: ComponentType<DocumentViewProps>;
52
+ }
53
+ const HOST_KEY = Symbol.for('vgai.editor.layout-host');
54
+ const hosts = globalThis as typeof globalThis & { [HOST_KEY]?: LayoutHost };
55
+ /** Installed once by the editor runtime; project modules share this SDK instance. */
56
+ export function registerLayoutHost(value: LayoutHost): void {
57
+ hosts[HOST_KEY] = value;
58
+ }
59
+ function currentHost(): LayoutHost {
60
+ const host = hosts[HOST_KEY];
61
+ if (!host) throw new Error('Editor layouts require an editor host.');
62
+ return host;
63
+ }
64
+ export function EditorFrame(props: LayoutFrameProps) {
65
+ return createElement(currentHost().Frame, props);
66
+ }
67
+ export function EditorHeader() {
68
+ return createElement(currentHost().Header);
69
+ }
70
+ export function EditorFooter() {
71
+ return createElement(currentHost().Footer);
72
+ }
73
+ export function Workspace(props: WorkspaceProps) {
74
+ return createElement(currentHost().Workspace, props);
75
+ }
76
+ export function DocumentView(props: DocumentViewProps) {
77
+ return createElement(currentHost().Document, props);
78
+ }
79
+
80
+ /** Full game authoring, with the running game taking over the workspace during Play. */
81
+ export function GameLayout() {
82
+ return (
83
+ <EditorFrame>
84
+ <EditorHeader />
85
+ <Workspace immersivePlay />
86
+ <EditorFooter />
87
+ </EditorFrame>
88
+ );
89
+ }
90
+ /** Studio keeps its authoring panels and opens analytics while the game runs. */
91
+ export function StudioLayout() {
92
+ return (
93
+ <EditorFrame>
94
+ <EditorHeader />
95
+ <Workspace immersivePlay={false} playUtilities={['tool:analytics.analytics']} />
96
+ <EditorFooter />
97
+ </EditorFrame>
98
+ );
99
+ }
100
+ export function DesignLayout() {
101
+ return (
102
+ <EditorFrame>
103
+ <EditorHeader />
104
+ <Workspace arrangement={DesignArrangement} immersivePlay={false} />
105
+ <EditorFooter />
106
+ </EditorFrame>
107
+ );
108
+ }
package/src/looks.ts ADDED
@@ -0,0 +1,14 @@
1
+ /** Contribution data types are owned by the project adapter contract. */
2
+ export type {
3
+ EditorKeyActionId,
4
+ KeyChord,
5
+ KeymapBindings,
6
+ KeymapContribution,
7
+ WorkspaceLayoutRegions,
8
+ WorkspaceLayoutContribution,
9
+ MaterialContribution,
10
+ DensityContribution,
11
+ IconCategoryTone,
12
+ IconSetContribution,
13
+ StyleContribution,
14
+ } from '@volter/editor-project/adapter/editor-looks';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The project directories a tool may commit generated bytes into, and the one
3
+ * normalizer every writer validates a path with.
4
+ *
5
+ * A project has two of them, and the difference is what SHIPS:
6
+ *
7
+ * - `public/` — game assets. Exported, served at a root-relative URL,
8
+ * loaded by the game's own source.
9
+ * - `references/` — reference material: moodboards, style plates, generated
10
+ * stills and clips an author or an agent looks at while
11
+ * building. Ordinary files, indexed by the editor's Content
12
+ * panel like any other media, and NOT exported — the
13
+ * project's own build copies `publicDir` and nothing else.
14
+ *
15
+ * This lives in the SDK rather than beside one writer because the rule has more
16
+ * than one enforcer: the editor host's atomic project-output writer, and every
17
+ * registered provider boundary that takes an `outputDirectory` from a caller.
18
+ * Two copies of "must be under public/" is how a reference output becomes
19
+ * writable through one door and refused at the next.
20
+ */
21
+
22
+ /** The writable output roots, in the order a chooser should offer them. */
23
+ export const PROJECT_OUTPUT_ROOTS = ['public', 'references'] as const;
24
+
25
+ export type ProjectOutputRoot = (typeof PROJECT_OUTPUT_ROOTS)[number];
26
+
27
+ /** Human-readable list for an error message: `public/` or `references/`. */
28
+ function rootList(): string {
29
+ return PROJECT_OUTPUT_ROOTS.map((root) => `${root}/`).join(' or ');
30
+ }
31
+
32
+ /** POSIX-normalized, `./`-stripped, trailing-slash-stripped. */
33
+ function normalizeSlashes(path: string): string {
34
+ const slashed = path.replaceAll('\\', '/');
35
+ const segments: string[] = [];
36
+ for (const segment of slashed.split('/')) {
37
+ if (segment === '' || segment === '.') continue;
38
+ segments.push(segment);
39
+ }
40
+ return segments.join('/');
41
+ }
42
+
43
+ /** The root `path` sits under, or `undefined` when it sits under none. */
44
+ export function projectOutputRootOf(path: string): ProjectOutputRoot | undefined {
45
+ const normalized = normalizeSlashes(path);
46
+ return PROJECT_OUTPUT_ROOTS.find(
47
+ (root) => normalized === root || normalized.startsWith(`${root}/`),
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Validate and normalize a project-relative output path.
53
+ *
54
+ * Throws — with a message naming BOTH roots, because the common mistake is
55
+ * knowing about one of them — for an absolute path, an escape, a bare root
56
+ * with no file under it, or anything outside the roots above. `subject` names
57
+ * what is being validated in that message (`Generated output path`, `Fal
58
+ * outputDirectory`, …).
59
+ */
60
+ export function normalizeProjectOutputPath(
61
+ path: string,
62
+ subject = 'Generated output path',
63
+ ): string {
64
+ const normalized = normalizeSlashes(path);
65
+ const escapes = normalized.split('/').includes('..');
66
+ const root = projectOutputRootOf(normalized);
67
+ if (path.startsWith('/') || escapes || root === undefined || normalized === root) {
68
+ throw new Error(
69
+ `${subject} ${JSON.stringify(path)} is invalid; it must be a project-relative path under ${rootList()}.`,
70
+ );
71
+ }
72
+ return normalized;
73
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * THE TAB RESOURCE CENSUS — one declaration of the profile a browser tab
3
+ * reports about itself, for every compilation unit that speaks it.
4
+ *
5
+ * WHAT IT IS. What a game tab is holding: JS heap, canvas backing stores,
6
+ * renderer resource counts, and how many project mount generations the
7
+ * document has accumulated. Sampled by the page every five seconds and carried
8
+ * on the heartbeat, so that a browser-level renderer death — which the presence
9
+ * system recovers from perfectly and would otherwise leave unexplained — has a
10
+ * number beside it. MEASUREMENT ONLY: nothing anywhere enforces a budget from
11
+ * these. The page-side sampler and its `why` are
12
+ * `packages/editor/src/tab-census.ts`.
13
+ *
14
+ * WHY IT LIVES HERE. This one shape crosses four compilation units — the page
15
+ * that samples it (`@vgai/editor`'s browser bundle), the editor server that
16
+ * files it (`@vgai/editor`'s node half), the editor-extension surface that
17
+ * describes the status response (`@volter/editor-sdk`), and the session journal
18
+ * plus the CLI row that print it (`@vgai/sdk`, `@vgai/cli`). Each of them used
19
+ * to spell the seven fields out again. `@vgai/sdk` is the only package all four
20
+ * already depend on: `@volter/editor-sdk` depends on `@vgai/sdk` and `@vgai/editor`
21
+ * depends on both, so this cannot live in `@volter/editor-sdk` without a cycle —
22
+ * and it does not belong in `@vgai/game-runtime`, whose subject is a running game,
23
+ * not an editor session's tabs.
24
+ *
25
+ * Deliberately import-free so a browser bundle can take it: the census's other
26
+ * `@vgai/sdk` home, `project/session-journal.ts`, reads `node:fs`.
27
+ *
28
+ * WHAT ELSE RIDES IT. The beat is the one channel that still moves when the
29
+ * page's main thread or the Blender worker is blocked, so `blender` — how long
30
+ * the worker's calls are taking and how long the main thread has been stalled —
31
+ * is carried here too (absent unless a Blender session exists in the tab). Same
32
+ * rule as the rest of this file: measurement only, no budget anywhere.
33
+ *
34
+ * ABSENT IS NOT ZERO. `heapUsedMB`/`heapLimitMB` are null off Chromium
35
+ * (`performance.memory` is non-standard). The renderer counts are ABSENT rather
36
+ * than zero when no game has registered a render-debug adapter, and `programs`
37
+ * is absent until a first render: a zero would read as "no textures", which is
38
+ * a different claim from "nobody measured".
39
+ */
40
+
41
+ /**
42
+ * THE BLENDER WORKER'S STALLS, as numbers.
43
+ *
44
+ * WHY (measured 2026-09-16): one `blender-execute` held the Blender worker for
45
+ * over 1,800s and wedged the tab, and a 0.86s-per-call scene stretched into
46
+ * heartbeat timeouts that read as "tab present, did not respond". The product
47
+ * had no number for either. Owner ruling: a tab that stops answering is the
48
+ * product's defect regardless of what the machine is doing, and the product has
49
+ * to surface it — so these ride the census to `vgai status`.
50
+ *
51
+ * MEASURED BY THE PAGE, because neither blocked party can report on itself: the
52
+ * worker's own loop is what is stuck, and a stalled main thread cannot send.
53
+ * The call half comes from `BlenderRuntime.metrics()`
54
+ * (`packages/blender-engine/browser/runtime.ts`), the stall half from a `longtask`
55
+ * PerformanceObserver (`packages/editor/src/blender-tab-metrics.ts`).
56
+ *
57
+ * MEASUREMENT ONLY. No threshold here cancels, kills or budgets a call.
58
+ */
59
+ export interface BlenderTabMetrics {
60
+ /** Age of the oldest OUTSTANDING worker call, or null when the worker is idle.
61
+ * The only field with a number during a wedge. */
62
+ readonly inFlightMs: number | null;
63
+ /** Duration of the newest completed call; null before the first one. */
64
+ readonly lastCallMs: number | null;
65
+ /** The longest call this page has seen, counting an outstanding one. */
66
+ readonly maxCallMs: number | null;
67
+ /** Calls past 5s, and past 30s, since this page loaded. */
68
+ readonly callsOver5s: number;
69
+ readonly callsOver30s: number;
70
+ /** Longest `longtask` entry since page load, in ms; null off Chromium (the
71
+ * Long Tasks API is not implemented everywhere) — never 0, which would read
72
+ * as "the main thread never stalled". */
73
+ readonly longestTaskMs: number | null;
74
+ /** How many long tasks ran past 100ms since page load. */
75
+ readonly tasksOver100ms: number;
76
+ /** The longest long task that overlapped the newest call's window — what the
77
+ * MAIN thread was doing while the worker was busy. Null when there has been
78
+ * no call yet, or no long task during it. */
79
+ readonly lastCallLongestTaskMs: number | null;
80
+ /**
81
+ * THE ENGINE'S OWN MEMORY: the Blender module's linear memory in MB, read in
82
+ * the worker (`packages/blender-engine/browser/worker.ts`) and posted after every
83
+ * call. Null before the session's first call, and absent against a page that
84
+ * predates the field.
85
+ *
86
+ * It is here because no other number on this census answers the question.
87
+ * `heapUsedMB` is the PAGE's JS heap — a few tens of MB — while the engine's
88
+ * memory is the larger half of the tab by far, and a reader looking at the
89
+ * heap line alone concludes the tab is cheap. wasm32 memory never shrinks, so
90
+ * this is simultaneously the current size and the session's high-water mark.
91
+ *
92
+ * MEASURED 2026-09-18 on this bundle: 512 MB reserved at boot of which ~166
93
+ * MB is ever touched, reaching 1036 MB across `17-workshop-interior`. The
94
+ * reservation costs no resident pages; the growth does, permanently.
95
+ */
96
+ readonly wasmMemoryMB?: number | null;
97
+ }
98
+
99
+ /**
100
+ * A census as a CURRENT page produces it and the server files it.
101
+ *
102
+ * Every field here is guaranteed by a producer that always writes it, and
103
+ * `packages/editor/server/tab-heartbeat.ts`'s `parseCensus` is where that is
104
+ * checked: a beat frame missing `canvases`, `canvasMB` or `mountEpochs` is
105
+ * rejected outright rather than filed as a partial profile.
106
+ */
107
+ export interface TabCensus {
108
+ /** `performance.memory.usedJSHeapSize` in MB; null off Chromium. */
109
+ readonly heapUsedMB: number | null;
110
+ /** `performance.memory.jsHeapSizeLimit` in MB; null off Chromium. */
111
+ readonly heapLimitMB: number | null;
112
+ /** Distinct project mount generations fetched by this document. */
113
+ readonly mountEpochs: number;
114
+ /** How many `<canvas>` elements the document holds. */
115
+ readonly canvases: number;
116
+ /** Their total pixel-buffer footprint: Σ width×height×4 bytes, in MB. */
117
+ readonly canvasMB: number;
118
+ /** `renderer.info.memory.textures` — absent with no mounted adapter. */
119
+ readonly textures?: number;
120
+ /** `renderer.info.memory.geometries` — absent with no mounted adapter. */
121
+ readonly geometries?: number;
122
+ /** Compiled programs — absent with no adapter, or before the first render. */
123
+ readonly programs?: number;
124
+ /** {@link BlenderTabMetrics} — absent unless this tab has a Blender session. */
125
+ readonly blender?: BlenderTabMetrics;
126
+ }
127
+
128
+ /**
129
+ * A census read back out of a RECORD, where the writer may predate a field.
130
+ *
131
+ * Exactly one field varies, and this is the only place that is said: a session
132
+ * journal line written before the mount census existed has no `mountEpochs`,
133
+ * and neither does a status response from an older editor server. The
134
+ * difference is a property of the READER's input, so it is named on the
135
+ * reader's type rather than by softening {@link TabCensus} — which is what the
136
+ * four hand-copied declarations did, and how the producers' own guarantee got
137
+ * lost on the way to the two SDKs.
138
+ *
139
+ * There is no runtime narrowing to point at because neither reader has a parse
140
+ * step to put one in: the journal is `JSON.parse` per line by recorded design
141
+ * (`session-journal.ts`: "nothing reads a journal back through a validator"),
142
+ * and the CLI reads the status body as JSON. So the absence is carried in the
143
+ * type and handled where it is printed — `formatTabCensus` omits the words
144
+ * rather than printing a fabricated count. Every {@link TabCensus} is a valid
145
+ * value of this type; the reverse is not.
146
+ */
147
+ export type RecordedTabCensus = Omit<TabCensus, 'mountEpochs'> & {
148
+ readonly mountEpochs?: number;
149
+ };
@@ -0,0 +1,96 @@
1
+ export type ToolContributionPoint =
2
+ | 'workspace.document'
3
+ | 'selection.inspector'
4
+ | 'asset.inspector'
5
+ | 'generation.result'
6
+ | 'workspace.utility'
7
+ | 'workspace.analytics'
8
+ | 'workspace.status';
9
+
10
+ /**
11
+ * One contribution module, FOUND BY SCANNING — never listed anywhere.
12
+ *
13
+ * Nothing enumerates these. `src/tools/` in project source and `src/` in every
14
+ * `vgai`-declaring dependency are walked for the naming convention
15
+ * (`*.document.tsx`, `*.inspector.tsx`, `*.asset-inspector.tsx`,
16
+ * `*.result.tsx`, `*.utility.tsx`, `*.analytics.tsx`), and the module itself declares everything
17
+ * else: `point`, `title` (or `presentations`), and the `tool` it drives, by
18
+ * name. Its id is derived from this path. A manifest cannot disagree with a
19
+ * module it does not mention.
20
+ */
21
+ export interface ProjectToolContribution {
22
+ /** Project-relative or package-absolute browser module path. */
23
+ entryPath: string;
24
+ /** The dependency that declared it (`package.json#vgai.contributions`),
25
+ * absent for the project's own `src/contributions/` modules. */
26
+ package?: string;
27
+ }
28
+
29
+ export interface ProjectToolErrorSummary {
30
+ code: string;
31
+ summary: string;
32
+ dataSchema?: unknown;
33
+ }
34
+
35
+ export interface ProjectToolCatalogEntry {
36
+ name: string;
37
+ summary: string;
38
+ description: string;
39
+ sourcePath: string;
40
+ inputSchema: unknown;
41
+ resultSchema: unknown;
42
+ errors: ProjectToolErrorSummary[];
43
+ requires: Record<string, boolean | undefined>;
44
+ host: 'node' | 'editor-browser' | 'runtime-page';
45
+ mutates: boolean;
46
+ supportsDryRun: boolean;
47
+ longRunning: boolean;
48
+ permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
49
+ generation?: { provider: string; role: 'submit' | 'poll' | 'cancel' | 'accept' };
50
+ }
51
+
52
+ export interface ProjectToolLoadError {
53
+ sourcePath: string;
54
+ message: string;
55
+ }
56
+
57
+ /**
58
+ * What a host says when it cannot load the project's TypeScript at all.
59
+ *
60
+ * ONE owner for the sentence, because two realms reach this state through
61
+ * different doors and both must say the same true thing rather than fall back
62
+ * to a fabricated empty catalog:
63
+ *
64
+ * - the NODE side (`packages/editor/server/project-tools.ts`) when a host
65
+ * builds the catalog with no `loadModule`; and
66
+ * - the PAGE side (`packages/editor/src/project-tools.ts`), whose fetch of
67
+ * `/__editor/project-tools` can be answered by an SPA FALLBACK — `200` and
68
+ * HTML — when no editor server is in front of the origin, so an unguarded
69
+ * fetch that merely checks `response.ok` reads a successful page load as a
70
+ * successful catalog, then dies in
71
+ * `res.json()`. The panel's empty state ("No project tools registered")
72
+ * is a FACT about the project; a host that cannot ask has not learned it.
73
+ */
74
+ export const NO_PROJECT_MODULE_HOST_MESSAGE =
75
+ 'This editor host cannot RUN registered project tools (package.json#vgai.tools): callables ' +
76
+ 'execute on the Node side. Contribution panels still load from project source; to run the ' +
77
+ 'tools themselves, open the project in the Vite-backed dev or packaged editor.';
78
+
79
+ /** The catalog row that states {@link NO_PROJECT_MODULE_HOST_MESSAGE}. */
80
+ export function noProjectModuleHostError(): ProjectToolLoadError {
81
+ return { sourcePath: 'package.json#vgai.tools', message: NO_PROJECT_MODULE_HOST_MESSAGE };
82
+ }
83
+
84
+ /**
85
+ * Callables and contribution modules are SIBLINGS, not parent and child.
86
+ *
87
+ * They used to be nested — `tools[].contributions[]` — purely so the browser
88
+ * loader had a tool to hand the component. A document is not owned by one
89
+ * callable, and the nesting made project source structurally unable to present
90
+ * a dependency-provided tool. The module names the tool it drives instead.
91
+ */
92
+ export interface ProjectToolCatalog {
93
+ tools: ProjectToolCatalogEntry[];
94
+ contributions: ProjectToolContribution[];
95
+ loadErrors: ProjectToolLoadError[];
96
+ }