Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions client/packages/editor-oss/src/agent/CommandsRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import AIWorldController from "../controls/AiWorldController/AiWorldController";
import EngineRuntime from "../EngineRuntime";
import global from "../global";
import {AssetHandlers} from "./handlers/AssetHandlers";
import {BehaviorHandlers} from "./handlers/BehaviorHandlers";
import {LambdaHandlers} from "./handlers/LambdaHandlers";
import {LightHandlers} from "./handlers/LightHandlers";
import {ObjectHandlers} from "./handlers/ObjectHandlers";
import {PhysicsHandlers} from "./handlers/PhysicsHandlers";
Expand All @@ -27,8 +29,10 @@ export class CommandsRegistry {
private aiWorldController = AIWorldController.getInstance(this.engine);

// Handler instances
private assetHandlers: AssetHandlers;
private objectHandlers: ObjectHandlers;
private behaviorHandlers: BehaviorHandlers;
private lambdaHandlers: LambdaHandlers;
private prefabHandlers: PrefabHandlers;
private physicsHandlers: PhysicsHandlers;
private vfxHandlers: VFXHandlers;
Expand All @@ -37,8 +41,10 @@ export class CommandsRegistry {
private taskHandlers: TaskHandlers;

constructor(options: CommandsRegistryOptions = {}) {
this.assetHandlers = new AssetHandlers(this.engine);
this.objectHandlers = new ObjectHandlers(this.engine, this.aiWorldController);
this.behaviorHandlers = new BehaviorHandlers(this.engine);
this.lambdaHandlers = new LambdaHandlers(this.engine);
this.prefabHandlers = new PrefabHandlers(this.engine);
this.physicsHandlers = new PhysicsHandlers(this.engine);
this.vfxHandlers = new VFXHandlers(this.engine);
Expand Down Expand Up @@ -331,6 +337,54 @@ export class CommandsRegistry {
handler: () => Promise.resolve(this.objectHandlers.handleGetPlayer()),
});

this.registerCommand({
name: SupportedCommands.ListSceneAssets,
description:
"List imported scene/stem assets such as models, behavior packs, lambda packs, script imports, files, media, VFX, and prefabs",
parameters: [
{
name: "type",
type: "string",
description:
"Optional asset type filter: all, model/models, import/imports/script/scripts, file/files, behavior/behaviors, lambda/lambdas, pack/packs, media, image, audio, video, vfx, prefab/stem",
required: false,
},
{name: "filter", type: "string", description: "Optional filter by id, name, description, tag, or type", required: false},
{name: "limit", type: "number", description: "Maximum assets to return (default 80, max 200)", required: false},
],
handler: params => this.assetHandlers.handleListSceneAssets(params),
});

this.registerCommand({
name: SupportedCommands.GetSceneAsset,
description: "Get compact metadata for one imported scene/stem asset by assetId, revisionId, or name",
parameters: [
{name: "assetId", type: "string", description: "Asset id, revision id, or exact name", required: false},
{name: "name", type: "string", description: "Asset name when assetId is omitted", required: false},
{name: "type", type: "string", description: "Optional asset type filter", required: false},
],
handler: params => this.assetHandlers.handleGetSceneAsset(params),
});

this.registerCommand({
name: SupportedCommands.ListLambdas,
description: "List all available lambdas with optional filtering",
parameters: [
{name: "filter", type: "string", description: "Optional filter by id or name pattern", required: false},
],
handler: params => Promise.resolve(this.lambdaHandlers.handleListLambdas(params)),
});

this.registerCommand({
name: SupportedCommands.GetLambda,
description: "Get detailed information about a specific lambda by ID, optionally including code when available",
parameters: [
{name: "lambdaId", type: "string", description: "ID of the lambda", required: true},
{name: "includeCode", type: "boolean", description: "Try to include lambda source code", required: false},
],
handler: params => this.lambdaHandlers.handleGetLambda(params),
});

// ===== MATERIAL & TEXTURE COMMANDS =====
this.registerCommand({
name: SupportedCommands.SetMaterial,
Expand Down Expand Up @@ -1408,6 +1462,10 @@ export enum SupportedCommands {
GetBehaviorSettings = "get_behavior_settings",
GetSelectedObject = "get_selected_object",
GetPlayer = "get_player",
ListSceneAssets = "list_scene_assets",
GetSceneAsset = "get_scene_asset",
ListLambdas = "list_lambdas",
GetLambda = "get_lambda",
SetMaterial = "set_material",
SetTexture = "set_texture",
SetExternalTexture = "set_external_texture",
Expand Down
104 changes: 104 additions & 0 deletions client/packages/editor-oss/src/agent/handlers/AssetHandlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {describe, expect, it, vi} from "vitest";

import {AssetHandlers} from "./AssetHandlers";

const createHandlers = () => {
const assetSource = {
kind: "scene",
id: "scene-1",
getAssets: vi.fn(async ({types}: {types?: string[]} = {}) => {
const assets = [
{
id: "model-1",
name: "Kart",
type: "model",
description: "A drivable kart model",
tags: ["vehicle"],
contentType: "model/gltf-binary",
format: "glb",
headRevisionId: "model-rev",
thumbnailUrl: "data:image/png;base64,large",
},
{
id: "script-1",
name: "math-helpers",
type: "script",
description: "Reusable movement helpers",
tags: ["import"],
headRevisionId: "script-rev",
},
{
id: "file-1",
name: "level-data.json",
type: "file",
description: "Level data",
headRevisionId: "file-rev",
},
{
id: "lambda-1",
name: "Patrol Brain",
type: "lambda",
description: "Patrol lambda pack",
headRevisionId: "lambda-rev",
},
];
return {
assets: types?.length ? assets.filter(asset => types.includes(asset.type)) : assets,
};
}),
};
const engine = {
editor: {assetSource},
} as any;

return {assetSource, handlers: new AssetHandlers(engine)};
};

describe("AssetHandlers", () => {
it("lists compact scene asset metadata by semantic type", async () => {
const {assetSource, handlers} = createHandlers();

const result = await handlers.handleListSceneAssets({type: "models"});

expect(assetSource.getAssets).toHaveBeenCalledWith({
types: ["model"],
includeLatestRelease: true,
includeThumbnails: true,
});
expect(result.status).toBe("success");
expect(result.data).toEqual(expect.objectContaining({
assetSource: {kind: "scene", id: "scene-1"},
total: 1,
assets: [
expect.objectContaining({
id: "model-1",
name: "Kart",
type: "model",
hasThumbnail: true,
}),
],
}));
expect(JSON.stringify(result.data)).not.toContain("data:image/png");
});

it("gets imports, files, and lambda packs by name or id", async () => {
const {handlers} = createHandlers();

const importResult = await handlers.handleGetSceneAsset({name: "math-helpers", type: "imports"});
const fileResult = await handlers.handleGetSceneAsset({assetId: "file-1", type: "files"});
const lambdaResult = await handlers.handleGetSceneAsset({name: "Patrol Brain", type: "lambdas"});

expect(importResult.status).toBe("success");
expect(importResult.data).toEqual(expect.objectContaining({
asset: expect.objectContaining({id: "script-1", type: "script"}),
}));
expect(fileResult.status).toBe("success");
expect(fileResult.data).toEqual(expect.objectContaining({
asset: expect.objectContaining({id: "file-1", type: "file"}),
}));
expect(lambdaResult.status).toBe("success");
expect(lambdaResult.data).toEqual(expect.objectContaining({
asset: expect.objectContaining({id: "lambda-1", type: "lambda"}),
}));
});
});
Loading
Loading