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
47 changes: 47 additions & 0 deletions ide/base/client/src/commands/language-model-tools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Commands,
RequestTypes,
WorkspaceContextSummary,
WorkspaceResourceSummary,
WorkspaceResourceType,
WorkspaceResourcesRequest,
Expand All @@ -27,6 +28,7 @@ const ToolNames = {
workspaceEntities: 'blockception.minecraft.workspaceEntities',
currentFileDiagnostics: 'blockception.minecraft.currentFileDiagnostics',
scaffoldProjectFiles: 'blockception.minecraft.scaffoldProjectFiles',
bedrockContext: 'blockception.minecraft.bedrockContext',
} as const;

const AllowedScaffoldCommands = new Set<string>([Commands.MCProject.Create]);
Expand Down Expand Up @@ -65,6 +67,40 @@ function isScaffoldCommand(command: string): boolean {
return command.startsWith(Commands.Create.Base) || AllowedScaffoldCommands.has(command);
}

function formatBedrockContext(ctx: WorkspaceContextSummary): string {
const lines: string[] = ['## Bedrock Project Context'];

if (ctx.packs.length > 0) {
lines.push('\n### Packs');
for (const pack of ctx.packs) {
const label = pack.type === 'behaviorPack' ? 'Behavior Pack' : pack.type === 'resourcePack' ? 'Resource Pack' : 'World';
lines.push(`- ${pack.name} (${label})`);
}
}

if (ctx.namespaces.length > 0) {
lines.push('\n### Project Namespaces');
lines.push(ctx.namespaces.join(', '));
}

if (ctx.entities.length > 0) {
lines.push(`\n### Entities (${ctx.entities.length})`);
lines.push(ctx.entities.join(', '));
}

if (ctx.blocks.length > 0) {
lines.push(`\n### Blocks (${ctx.blocks.length})`);
lines.push(ctx.blocks.join(', '));
}

if (ctx.items.length > 0) {
lines.push(`\n### Items (${ctx.items.length})`);
lines.push(ctx.items.join(', '));
}

return lines.join('\n');
}

export function activate(context: vscode.ExtensionContext): void {
if (!isLanguageModelToolsSupported()) return;

Expand Down Expand Up @@ -168,5 +204,16 @@ export function activate(context: vscode.ExtensionContext): void {
});
},
}),
vscode.lm.registerTool(ToolNames.bedrockContext, {
async invoke() {
if (!Manager.Client) {
return toToolResult({ error: 'Minecraft language client is not available yet.' });
}

const ctx = await Manager.Client.sendRequest<WorkspaceContextSummary>(RequestTypes.WorkspaceContext);

return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(formatBedrockContext(ctx))]);
},
}),
);
}
2 changes: 2 additions & 0 deletions ide/base/server/src/lsp/dataset/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const emptyCollection: { forEach(callbackfn: (item: { id?: string }) => void): v
function createProjectData(): WorkspaceProjectDataCollections {
return {
behaviorPacks: {
packs: [],
entities: emptyCollection,
items: emptyCollection,
blocks: emptyCollection,
Expand All @@ -28,6 +29,7 @@ function createProjectData(): WorkspaceProjectDataCollections {
itemGroups: emptyCollection,
},
resourcePacks: {
packs: [],
entities: emptyCollection,
animations: emptyCollection,
animationControllers: emptyCollection,
Expand Down
50 changes: 49 additions & 1 deletion ide/base/server/src/lsp/language-model-tools/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getWorkspaceResourceSummaries, WorkspaceProjectDataCollections } from './service';
import { getWorkspaceContextSummary, getWorkspaceResourceSummaries, WorkspaceProjectDataCollections } from './service';

const emptyCollection: { forEach(callbackfn: (item: { id?: string }) => void): void } = {
forEach(): void {
Expand All @@ -9,6 +9,7 @@ const emptyCollection: { forEach(callbackfn: (item: { id?: string }) => void): v
function createProjectData(): WorkspaceProjectDataCollections {
return {
behaviorPacks: {
packs: [],
entities: emptyCollection,
items: emptyCollection,
blocks: emptyCollection,
Expand All @@ -26,6 +27,7 @@ function createProjectData(): WorkspaceProjectDataCollections {
itemGroups: emptyCollection,
},
resourcePacks: {
packs: [],
entities: emptyCollection,
animations: emptyCollection,
animationControllers: emptyCollection,
Expand Down Expand Up @@ -87,3 +89,49 @@ describe('language-model-tools service', () => {
]);
});
});

describe('getWorkspaceContextSummary', () => {
it('returns empty collections when project has no data', () => {
const result = getWorkspaceContextSummary(createProjectData());
expect(result).toEqual({ packs: [], namespaces: [], entities: [], blocks: [], items: [] });
});

it('lists behavior pack and resource pack names from their folders', () => {
const projectData = createProjectData();
projectData.behaviorPacks.packs = [{ folder: '/workspace/behavior_packs/my_bp' }];
projectData.resourcePacks.packs = [{ folder: '/workspace/resource_packs/my_rp' }];

const result = getWorkspaceContextSummary(projectData);
expect(result.packs).toEqual([
{ type: 'behaviorPack', name: 'my_bp' },
{ type: 'resourcePack', name: 'my_rp' },
]);
});

it('derives namespaces from entity, block and item ids excluding "minecraft"', () => {
const projectData = createProjectData();
projectData.behaviorPacks.entities = [{ id: 'mymod:zombie' }, { id: 'minecraft:creeper' }];
projectData.behaviorPacks.blocks = [{ id: 'mymod:custom_block' }];
projectData.behaviorPacks.items = [{ id: 'othermod:sword' }];

const result = getWorkspaceContextSummary(projectData);
expect(result.namespaces).toEqual(['mymod', 'othermod']);
});

it('collects and deduplicates entity ids from both bp and rp', () => {
const projectData = createProjectData();
projectData.behaviorPacks.entities = [{ id: 'example:zombie' }, { id: 'example:bee' }];
projectData.resourcePacks.entities = [{ id: 'example:bee' }, { id: 'example:creeper' }];

const result = getWorkspaceContextSummary(projectData);
expect(result.entities).toEqual(['example:bee', 'example:creeper', 'example:zombie']);
});

it('extracts pack name from Windows-style backslash path', () => {
const projectData = createProjectData();
projectData.behaviorPacks.packs = [{ folder: 'C:\\workspace\\behavior_packs\\win_bp' }];

const result = getWorkspaceContextSummary(projectData);
expect(result.packs).toEqual([{ type: 'behaviorPack', name: 'win_bp' }]);
});
});
83 changes: 73 additions & 10 deletions ide/base/server/src/lsp/language-model-tools/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
RequestTypes,
WorkspaceContextSummary,
WorkspacePackSummary,
WorkspaceResourceSource,
WorkspaceResourceSummary,
WorkspaceResourceType,
Expand All @@ -15,8 +17,11 @@ type CollectionWithIds = {
forEach(callbackfn: (item: { id?: string }) => void): void;
};

type PackEntry = { readonly folder: string };

export interface WorkspaceProjectDataCollections {
behaviorPacks: {
packs: ReadonlyArray<PackEntry>;
entities: CollectionWithIds;
items: CollectionWithIds;
blocks: CollectionWithIds;
Expand All @@ -34,6 +39,7 @@ export interface WorkspaceProjectDataCollections {
itemGroups: CollectionWithIds;
};
resourcePacks: {
packs: ReadonlyArray<PackEntry>;
entities: CollectionWithIds;
animations: CollectionWithIds;
animationControllers: CollectionWithIds;
Expand Down Expand Up @@ -72,6 +78,9 @@ export class LanguageModelToolService extends BaseService implements IService {
connection.onRequest(RequestTypes.WorkspaceEntities, (params: WorkspaceResourcesRequest | undefined) =>
this.onWorkspaceResourcesRequest(params),
),
connection.onRequest(RequestTypes.WorkspaceContext, () =>
getWorkspaceContextSummary(this.extension.database.ProjectData),
),
);
}

Expand Down Expand Up @@ -118,44 +127,47 @@ export function getWorkspaceResourceSummaries(
type CollectionEntry = { source: WorkspaceResourceSource; items: CollectionWithIds };
type CollectionSelector = (projectData: WorkspaceProjectDataCollections) => CollectionEntry[];

const behaviorPackCollection = <K extends keyof WorkspaceProjectDataCollections['behaviorPacks']>(
/** Keys of a type whose values extend CollectionWithIds */
type CollectionKeys<T> = { [K in keyof T]: T[K] extends CollectionWithIds ? K : never }[keyof T];

const behaviorPackCollection = <K extends CollectionKeys<WorkspaceProjectDataCollections['behaviorPacks']>>(
key: K,
): CollectionSelector => {
return (projectData) => [{ source: 'behaviorPack', items: projectData.behaviorPacks[key] }];
return (projectData) => [{ source: 'behaviorPack', items: projectData.behaviorPacks[key] as CollectionWithIds }];
};

const resourcePackCollection = <K extends keyof WorkspaceProjectDataCollections['resourcePacks']>(
const resourcePackCollection = <K extends CollectionKeys<WorkspaceProjectDataCollections['resourcePacks']>>(
key: K,
): CollectionSelector => {
return (projectData) => [{ source: 'resourcePack', items: projectData.resourcePacks[key] }];
return (projectData) => [{ source: 'resourcePack', items: projectData.resourcePacks[key] as CollectionWithIds }];
};

const generalCollection = <K extends keyof WorkspaceProjectDataCollections['general']>(key: K): CollectionSelector => {
return (projectData) => [{ source: 'general', items: projectData.general[key] }];
};

const behaviorAndResourceCollection = <
BK extends keyof WorkspaceProjectDataCollections['behaviorPacks'],
RK extends keyof WorkspaceProjectDataCollections['resourcePacks'],
BK extends CollectionKeys<WorkspaceProjectDataCollections['behaviorPacks']>,
RK extends CollectionKeys<WorkspaceProjectDataCollections['resourcePacks']>,
>(
behaviorPackKey: BK,
resourcePackKey: RK,
): CollectionSelector => {
return (projectData) => [
{ source: 'behaviorPack', items: projectData.behaviorPacks[behaviorPackKey] },
{ source: 'resourcePack', items: projectData.resourcePacks[resourcePackKey] },
{ source: 'behaviorPack', items: projectData.behaviorPacks[behaviorPackKey] as CollectionWithIds },
{ source: 'resourcePack', items: projectData.resourcePacks[resourcePackKey] as CollectionWithIds },
];
};

const behaviorAndGeneralCollection = <
BK extends keyof WorkspaceProjectDataCollections['behaviorPacks'],
BK extends CollectionKeys<WorkspaceProjectDataCollections['behaviorPacks']>,
GK extends keyof WorkspaceProjectDataCollections['general'],
>(
behaviorPackKey: BK,
generalKey: GK,
): CollectionSelector => {
return (projectData) => [
{ source: 'behaviorPack', items: projectData.behaviorPacks[behaviorPackKey] },
{ source: 'behaviorPack', items: projectData.behaviorPacks[behaviorPackKey] as CollectionWithIds },
{ source: 'general', items: projectData.general[generalKey] },
];
};
Expand Down Expand Up @@ -193,3 +205,54 @@ const resourceTypeCollections: Record<WorkspaceResourceType, CollectionSelector>
tags: generalCollection('tags'),
tickingAreas: generalCollection('tickingAreas'),
};

/**
* Returns a consolidated Bedrock project context summary from loaded project data.
* Includes pack names, derived namespaces, and entity/block/item identifier lists.
*/
export function getWorkspaceContextSummary(projectData: WorkspaceProjectDataCollections): WorkspaceContextSummary {
const packs: WorkspacePackSummary[] = [];

for (const pack of projectData.behaviorPacks.packs) {
packs.push({ type: 'behaviorPack', name: folderName(pack.folder) });
}

for (const pack of projectData.resourcePacks.packs) {
packs.push({ type: 'resourcePack', name: folderName(pack.folder) });
}

const entities = collectUniqueIds(projectData.behaviorPacks.entities, projectData.resourcePacks.entities);
const blocks = collectUniqueIds(projectData.behaviorPacks.blocks);
const items = collectUniqueIds(projectData.behaviorPacks.items);

const namespaceSet = new Set<string>();
for (const id of [...entities, ...blocks, ...items]) {
const colon = id.indexOf(':');
if (colon > 0) {
const ns = id.substring(0, colon);
if (ns !== 'minecraft') namespaceSet.add(ns);
}
}

return {
packs,
namespaces: Array.from(namespaceSet).sort(),
entities,
blocks,
items,
};
}

function folderName(folder: string): string {
return folder.replace(/[/\\]+$/, '').split(/[/\\]/).pop() ?? folder;
}

function collectUniqueIds(...collections: CollectionWithIds[]): string[] {
const seen = new Set<string>();
for (const collection of collections) {
collection.forEach((item) => {
if (typeof item.id === 'string' && item.id.trim() !== '') seen.add(item.id);
});
}
return Array.from(seen).sort();
}
30 changes: 30 additions & 0 deletions ide/shared/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ export namespace RequestTypes {
export const DataSet: string = 'bc/minecraft/dataset';
/** The method for requesting workspace resource identifiers from the loaded workspace project data */
export const WorkspaceEntities: string = 'bc/minecraft/workspace/entities';
/** The method for requesting a consolidated Bedrock project context summary from the server */
export const WorkspaceContext: string = 'bc/minecraft/workspace/context';
}

/** Supported workspace project resource categories for language model tools. */
Expand Down Expand Up @@ -265,6 +267,34 @@ export interface WorkspaceResourceSummary {
type: WorkspaceResourceType;
}

/** A brief summary of a single loaded pack returned as part of {@link WorkspaceContextSummary}. */
export interface WorkspacePackSummary {
/** Whether this is a behavior pack, resource pack, or world. */
type: 'behaviorPack' | 'resourcePack' | 'world';
/** The folder name (last path segment) of the pack. */
name: string;
}

/**
* A consolidated snapshot of the loaded Bedrock project returned by
* {@link RequestTypes.WorkspaceContext}. Intended for use as Copilot Chat context.
*/
export interface WorkspaceContextSummary {
/** All behavior-pack and resource-pack folders currently loaded. */
packs: WorkspacePackSummary[];
/**
* Unique namespace prefixes found in entity, block, and item identifiers,
* excluding the built-in "minecraft" namespace.
*/
namespaces: string[];
/** All entity identifiers from behavior and resource packs. */
entities: string[];
/** All block identifiers from behavior packs. */
blocks: string[];
/** All item identifiers from behavior packs. */
items: string[];
}

/** Dataset identifiers for use with RequestTypes.DataSet */
export namespace DataSets {
/** MCP-compatible dataset endpoints */
Expand Down
11 changes: 11 additions & 0 deletions ide/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"onLanguageModelTool:blockception.minecraft.workspaceEntities",
"onLanguageModelTool:blockception.minecraft.currentFileDiagnostics",
"onLanguageModelTool:blockception.minecraft.scaffoldProjectFiles",
"onLanguageModelTool:blockception.minecraft.bedrockContext",
"onCommand:bc.mcproject.create",
"onCommand:bc-create-project-world",
"onCommand:bc-create-project-behavior-pack",
Expand Down Expand Up @@ -288,6 +289,16 @@
}
}
}
},
{
"name": "blockception.minecraft.bedrockContext",
"displayName": "Bedrock Context",
"modelDescription": "Returns a consolidated snapshot of the loaded Minecraft Bedrock project: loaded pack names, project namespaces, and lists of entity, block, and item identifiers. Use this to give Copilot project-specific context.",
"canBeReferencedInPrompt": true,
"inputSchema": {
"type": "object",
"properties": {}
}
}
],
"commands": [
Expand Down