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
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,11 @@ export function mapDiscoveredCustomizations(

const result: Customization[] = [];
for (const bucket of [...workspaceBuckets, userBucket]) {
if (bucket.agents.length > 0) {
result.push(makeDirectory(bucket.base, 'agents', CustomizationType.Agent, bucket.agents));
}
if (bucket.skills.length > 0) {
result.push(makeDirectory(bucket.base, 'skills', CustomizationType.Skill, bucket.skills));
}
if (bucket.rules.length > 0) {
result.push(makeDirectory(bucket.base, 'rules', CustomizationType.Rule, bucket.rules));
}
result.push(
makeDirectory(bucket.base, 'agents', CustomizationType.Agent, bucket.agents),
makeDirectory(bucket.base, 'skills', CustomizationType.Skill, bucket.skills),
makeDirectory(bucket.base, 'rules', CustomizationType.Rule, bucket.rules),
);
Comment on lines +154 to +158
if (bucket.hooks.length > 0) {
result.push(makeDirectory(bucket.base, 'hooks', CustomizationType.Hook, bucket.hooks));
}
Expand Down
26 changes: 22 additions & 4 deletions src/vs/platform/agentHost/test/node/claudeAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9116,7 +9116,16 @@ suite('ClaudeAgent — Phase 11 customizations', () => {
// always present pre-materialize for discoverability (before a live SDK
// set exists): the built-in agents directory and the "Built-in" skills
// container.
assert.deepStrictEqual(customizations.map(c => c.uri), ['https://a', 'file:///mock-home/.claude/agents', 'agent-builtin:/skills']);
assert.deepStrictEqual(customizations.map(c => c.uri), [
'https://a',
'file:///work/.claude/agents',
'file:///work/.claude/skills',
'file:///work/.claude/rules',
'file:///mock-home/.claude/agents',
'file:///mock-home/.claude/skills',
'file:///mock-home/.claude/rules',
'agent-builtin:/skills',
]);
});

test('getChatCustomizations overlays the enablement state onto client-pushed entries', async () => {
Expand Down Expand Up @@ -9346,7 +9355,16 @@ suite('ClaudeAgent — Phase 11 customizations', () => {
// the client-pushed entry survives (UI not blanked) and the curated
// built-ins are appended (the built-in agents directory and the skills
// container) since there is no live set to derive from.
assert.deepStrictEqual(customizations.map(c => c.uri), ['https://a', 'file:///mock-home/.claude/agents', 'agent-builtin:/skills'], 'client-pushed projection survives SDK snapshot failure');
assert.deepStrictEqual(customizations.map(c => c.uri), [
'https://a',
'file:///work/.claude/agents',
'file:///work/.claude/skills',
'file:///work/.claude/rules',
'file:///mock-home/.claude/agents',
'file:///mock-home/.claude/skills',
'file:///mock-home/.claude/rules',
'agent-builtin:/skills',
], 'client-pushed projection survives SDK snapshot failure');
});

test('getChatCustomizations derives the Built-in container from the live SDK command set post-materialize', async () => {
Expand All @@ -9368,8 +9386,8 @@ suite('ClaudeAgent — Phase 11 customizations', () => {
await agent.chats.sendMessage(defaultChatUri(created.session), 'first', undefined, undefined, 'turn-1', undefined, undefined, chatContext(defaultChatUri(created.session)));

const customizations = await agent.getChatCustomizations!(defaultChatUri(created.session), chatContext(defaultChatUri(created.session)), hostCustomizations(stateManager, created.session));
assert.strictEqual(customizations.length, 1);
const container = customizations[0];
const container = customizations.find(customization => customization.uri === 'agent-builtin:/skills');
assert.ok(container);
assert.strictEqual(container.type, CustomizationType.Directory);
assert.strictEqual(container.uri, 'agent-builtin:/skills');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ suite('claudeSessionCustomizationDiscovery', () => {

assert.deepStrictEqual(
(result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[])
.filter(directory => (directory.children?.length ?? 0) > 0)
.map(directory => ({ uri: directory.uri, children: directory.children?.map(child => child.name) })),
[
{ uri: URI.joinPath(workspace, '.claude', 'agents').toString(), children: ['root'] },
Expand All @@ -81,6 +82,7 @@ suite('claudeSessionCustomizationDiscovery', () => {

assert.deepStrictEqual(
(result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[])
.filter(directory => (directory.children?.length ?? 0) > 0)
.map(directory => ({ uri: directory.uri, children: directory.children?.map(child => child.name) })),
[
{ uri: URI.joinPath(userHome, '.claude', 'skills').toString(), children: ['user-skill'] },
Expand All @@ -97,6 +99,7 @@ suite('claudeSessionCustomizationDiscovery', () => {

assert.deepStrictEqual(
(result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[])
.filter(directory => (directory.children?.length ?? 0) > 0)
.map(directory => ({ uri: directory.uri, children: directory.children?.map(child => child.name) })),
[
{ uri: URI.joinPath(broadRoot, '.claude', 'skills').toString(), children: ['user-skill'] },
Expand All @@ -121,7 +124,9 @@ suite('claudeSessionCustomizationDiscovery', () => {
// Workspace containers first (agents, skills), then user — each rooted at
// the real `<scope>/.claude/<sub>` dir so the workbench can label scope.
assert.deepStrictEqual(
dirs.map(d => ({ uri: d.uri, contents: d.contents, children: d.children?.map(c => ({ name: c.name, uri: c.uri })) })),
dirs
.filter(directory => (directory.children?.length ?? 0) > 0)
.map(d => ({ uri: d.uri, contents: d.contents, children: d.children?.map(c => ({ name: c.name, uri: c.uri })) })),
[
{ uri: URI.joinPath(workspace, '.claude', 'agents').toString(), contents: CustomizationType.Agent, children: [{ name: 'wa', uri: wsAgentUri.toString() }] },
{ uri: URI.joinPath(workspace, '.claude', 'skills').toString(), contents: CustomizationType.Skill, children: [{ name: 'ws', uri: wsSkillUri.toString() }] },
Expand All @@ -147,7 +152,9 @@ suite('claudeSessionCustomizationDiscovery', () => {

const dirs = result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[];
assert.deepStrictEqual(
dirs.map(d => ({ uri: d.uri, contents: d.contents, children: d.children?.map(c => ({ name: c.name, uri: c.uri })) })),
dirs
.filter(directory => (directory.children?.length ?? 0) > 0)
.map(d => ({ uri: d.uri, contents: d.contents, children: d.children?.map(c => ({ name: c.name, uri: c.uri })) })),
[
{ uri: URI.joinPath(workspace, '.claude', 'rules').toString(), contents: CustomizationType.Rule, children: [{ name: 'CLAUDE.md', uri: wsRuleUri.toString() }] },
{ uri: URI.joinPath(userHome, '.claude', 'rules').toString(), contents: CustomizationType.Rule, children: [{ name: 'g', uri: userRuleUri.toString() }] },
Expand All @@ -164,14 +171,33 @@ suite('claudeSessionCustomizationDiscovery', () => {

const dirs = result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[];
assert.deepStrictEqual(
dirs.map(d => ({ uri: d.uri, contents: d.contents, children: d.children?.map(c => ({ name: c.name, uri: c.uri })) })),
dirs
.filter(directory => (directory.children?.length ?? 0) > 0)
.map(d => ({ uri: d.uri, contents: d.contents, children: d.children?.map(c => ({ name: c.name, uri: c.uri })) })),
[
{ uri: URI.joinPath(workspace, '.claude', 'hooks').toString(), contents: CustomizationType.Hook, children: [{ name: 'settings.json', uri: wsHookUri.toString() }] },
{ uri: URI.joinPath(userHome, '.claude', 'hooks').toString(), contents: CustomizationType.Hook, children: [{ name: 'settings.json', uri: userHookUri.toString() }] },
],
);
});

test('publishes empty workspace and user migration target directories', () => {
const result = mapDiscoveredCustomizations([], [], [], [], workspace, userHome);

const dirs = result.filter(c => c.type === CustomizationType.Directory) as DirectoryCustomization[];
assert.deepStrictEqual(
dirs.map(d => ({ uri: d.uri, contents: d.contents, writable: d.writable, children: d.children })),
[
{ uri: URI.joinPath(workspace, '.claude', 'agents').toString(), contents: CustomizationType.Agent, writable: true, children: [] },
{ uri: URI.joinPath(workspace, '.claude', 'skills').toString(), contents: CustomizationType.Skill, writable: true, children: [] },
{ uri: URI.joinPath(workspace, '.claude', 'rules').toString(), contents: CustomizationType.Rule, writable: true, children: [] },
{ uri: URI.joinPath(userHome, '.claude', 'agents').toString(), contents: CustomizationType.Agent, writable: true, children: [] },
{ uri: URI.joinPath(userHome, '.claude', 'skills').toString(), contents: CustomizationType.Skill, writable: true, children: [] },
{ uri: URI.joinPath(userHome, '.claude', 'rules').toString(), contents: CustomizationType.Rule, writable: true, children: [] },
],
);
});

test('maps a native plugin into a top-level Plugin container carrying its bundled children', () => {
const plugin = nativePlugin('tg@m', '/home/.claude/plugins/cache/m/tg/1.0.0', { skills: ['send'], agents: ['helper'] });

Expand Down