diff --git a/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts b/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts index 1021388a1c203..73f13d1e7d442 100644 --- a/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts +++ b/src/vs/platform/agentHost/node/claude/customizations/claudeSessionCustomizationDiscovery.ts @@ -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), + ); if (bucket.hooks.length > 0) { result.push(makeDirectory(bucket.base, 'hooks', CustomizationType.Hook, bucket.hooks)); } diff --git a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts index fde567e90763e..49486a77702e0 100644 --- a/src/vs/platform/agentHost/test/node/claudeAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/claudeAgent.test.ts @@ -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 () => { @@ -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 () => { @@ -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'); diff --git a/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts b/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts index 13961fb7c2f97..d900e0354d1f7 100644 --- a/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts +++ b/src/vs/platform/agentHost/test/node/customizations/claudeSessionCustomizationDiscovery.test.ts @@ -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'] }, @@ -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'] }, @@ -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'] }, @@ -121,7 +124,9 @@ suite('claudeSessionCustomizationDiscovery', () => { // Workspace containers first (agents, skills), then user — each rooted at // the real `/.claude/` 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() }] }, @@ -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() }] }, @@ -164,7 +171,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', '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() }] }, @@ -172,6 +181,23 @@ suite('claudeSessionCustomizationDiscovery', () => { ); }); + 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'] });