sessions: convert built-in prompts to skills and add UI Integration badge (#305347)

* sessions: convert built-in prompts to skills and add UI Integration badge

- Move all 6 built-in prompts from vs/sessions/prompts/ to
  vs/sessions/skills/{name}/SKILL.md with proper frontmatter
- Remove the built-in prompt discovery system (discoverBuiltinPrompts,
  getBuiltinPromptFiles, BUILTIN_PROMPTS_URI) from AgenticPromptsService
- Simplify listPromptFiles/listPromptFilesForStorage to only handle
  skills as the built-in type
- Add getSkillUIIntegrations() to IAICustomizationWorkspaceService
  interface, returning a map of skill names with UI surface connections
- Sessions implementation maps act-on-feedback (Submit Feedback button)
  and generate-run-commands (Run button) to tooltips
- Show 'UI Integration' badge in the customizations editor for skills
  that drive UI surfaces, including user overrides of those skills
- Update AI_CUSTOMIZATIONS.md to reflect the simplified architecture

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address review feedback: stable folder-name lookup, safe builtin fallback, shared empty map

- Use basename(dirname(skill.uri)) instead of skill.name for UI
  integration lookup so badge persists even if frontmatter name changes
- Return [] for BUILTIN_STORAGE on non-skill types instead of
  delegating to super (which would throw)
- Use a shared static empty map in the core VS Code implementation to
  avoid repeated allocations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* add built-in skills with UI integration badges for sessions

* add missing UI Integration mappings for all toolbar-connected skills

Add create-pr, create-draft-pr, update-pr, merge-changes, and commit
to the skill UI integrations map. These are all triggered by buttons
in the Changes toolbar.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Josh Spicer
2026-03-27 00:08:50 +00:00
committed by GitHub
parent 6e1a95ed84
commit 201c439f3b
13 changed files with 128 additions and 72 deletions

View File

@@ -172,20 +172,24 @@ The underlying `storage` remains `PromptsStorage.extension` — the grouping is
Sessions overrides `PromptsService` via `AgenticPromptsService` (in `promptsService.ts`):
- **Discovery**: `AgenticPromptFilesLocator` scopes workspace folders to the active session's worktree
- **Built-in prompts**: Discovers bundled `.prompt.md` files from `vs/sessions/prompts/` and surfaces them with `PromptsStorage.builtin` storage type
- **User override**: Built-in prompts are omitted when a user or workspace prompt with the same name exists
- **Built-in skills**: Discovers bundled `SKILL.md` files from `vs/sessions/skills/{name}/` and surfaces them with `PromptsStorage.builtin` storage type
- **User override**: Built-in skills are omitted when a user or workspace skill with the same name exists
- **Creation targets**: `getSourceFolders()` override replaces VS Code profile user roots with `~/.copilot/{subfolder}` for CLI compatibility
- **Hook folders**: Falls back to `.github/hooks` in the active worktree
### Built-in Prompts
### Built-in Skills
Prompt files bundled with the Sessions app live in `src/vs/sessions/prompts/`. They are:
All built-in customizations bundled with the Sessions app are skills, living in `src/vs/sessions/skills/{name}/SKILL.md`. They are:
- Discovered at runtime via `FileAccess.asFileUri('vs/sessions/prompts')`
- Discovered at runtime via `FileAccess.asFileUri('vs/sessions/skills')`
- Tagged with `PromptsStorage.builtin` storage type
- Shown in a "Built-in" group in the AI Customization tree view and management editor
- Filtered out when a user/workspace prompt shares the same clean name (override behavior)
- Included in storage filters for prompts and CLI-user types
- Filtered out when a user/workspace skill shares the same name (override behavior)
- Skills with UI integrations (e.g. `act-on-feedback`, `generate-run-commands`) display a "UI Integration" badge in the management editor
### UI Integration Badges
Skills that are directly invoked by UI elements (toolbar buttons, menu items) are annotated with a "UI Integration" badge in the management editor. The mapping is provided by `IAICustomizationWorkspaceService.getSkillUIIntegrations()`, which the Sessions implementation populates with the relevant skill names and tooltip descriptions. The badge appears on both the built-in skill and any user/workspace override, ensuring users understand that overriding the skill affects a UI surface.
### Count Consistency
@@ -201,7 +205,7 @@ Prompt files bundled with the Sessions app live in `src/vs/sessions/prompts/`. T
### Item Badges
`IAICustomizationListItem.badge` is an optional string that renders as a small inline tag next to the item name (same visual style as the MCP "Bridged" badge). For context instructions, this badge shows the raw `applyTo` pattern (e.g. a glob like `**/*.ts`), while the tooltip (`badgeTooltip`) explains the behavior. The badge text is also included in search filtering.
`IAICustomizationListItem.badge` is an optional string that renders as a small inline tag next to the item name (same visual style as the MCP "Bridged" badge). For context instructions, this badge shows the raw `applyTo` pattern (e.g. a glob like `**/*.ts`), while the tooltip (`badgeTooltip`) explains the behavior. For skills with UI integrations, the badge reads "UI Integration" with a tooltip describing which UI surface invokes the skill. The badge text is also included in search filtering.
### Debug Panel

View File

@@ -261,4 +261,18 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization
return applyStorageSourceFilter([cmd.promptPath], filter).length > 0;
});
}
private static readonly _skillUIIntegrations: ReadonlyMap<string, string> = new Map([
['act-on-feedback', localize('skillUI.actOnFeedback', "Used by the Submit Feedback button in the Changes toolbar")],
['generate-run-commands', localize('skillUI.generateRunCommands', "Used by the Run button in the title bar")],
['create-pr', localize('skillUI.createPr', "Used by the Create Pull Request button in the Changes toolbar")],
['create-draft-pr', localize('skillUI.createDraftPr', "Used by the Create Draft Pull Request button in the Changes toolbar")],
['update-pr', localize('skillUI.updatePr', "Used by the Update Pull Request button in the Changes toolbar")],
['merge-changes', localize('skillUI.mergeChanges', "Used by the Merge button in the Changes toolbar")],
['commit', localize('skillUI.commit', "Used by the Commit button in the Changes toolbar")],
]);
getSkillUIIntegrations(): ReadonlyMap<string, string> {
return SessionsAICustomizationWorkspaceService._skillUIIntegrations;
}
}

View File

@@ -14,7 +14,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common
import { IFileService } from '../../../../platform/files/common/files.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
import { HOOKS_SOURCE_FOLDER, SKILL_FILENAME, getCleanPromptName } from '../../../../workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.js';
import { HOOKS_SOURCE_FOLDER, SKILL_FILENAME } from '../../../../workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.js';
import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
import { IAgentSkill, IPromptPath, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { BUILTIN_STORAGE, IBuiltinPromptPath } from '../../chat/common/builtinPromptsStorage.js';
@@ -25,15 +25,11 @@ import { IUserDataProfileService } from '../../../../workbench/services/userData
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';
/** URI root for built-in prompts bundled with the Sessions app. */
export const BUILTIN_PROMPTS_URI = FileAccess.asFileUri('vs/sessions/prompts');
/** URI root for built-in skills bundled with the Sessions app. */
export const BUILTIN_SKILLS_URI = FileAccess.asFileUri('vs/sessions/skills');
export class AgenticPromptsService extends PromptsService {
private _copilotRoot: URI | undefined;
private _builtinPromptsCache: Map<PromptsType, Promise<readonly IBuiltinPromptPath[]>> | undefined;
private _builtinSkillsCache: Promise<readonly IAgentSkill[]> | undefined;
protected override createPromptFilesLocator(): PromptFilesLocator {
@@ -48,42 +44,6 @@ export class AgenticPromptsService extends PromptsService {
return this._copilotRoot;
}
/**
* Returns built-in prompt files bundled with the Sessions app.
*/
private async getBuiltinPromptFiles(type: PromptsType): Promise<readonly IBuiltinPromptPath[]> {
if (type !== PromptsType.prompt) {
return [];
}
if (!this._builtinPromptsCache) {
this._builtinPromptsCache = new Map();
}
let cached = this._builtinPromptsCache.get(type);
if (!cached) {
cached = this.discoverBuiltinPrompts(type);
this._builtinPromptsCache.set(type, cached);
}
return cached;
}
private async discoverBuiltinPrompts(type: PromptsType): Promise<readonly IBuiltinPromptPath[]> {
const fileService = this.instantiationService.invokeFunction(accessor => accessor.get(IFileService));
const promptsDir = FileAccess.asFileUri('vs/sessions/prompts');
try {
const stat = await fileService.resolve(promptsDir);
if (!stat.children) {
return [];
}
return stat.children
.filter(child => !child.isDirectory && child.name.endsWith('.prompt.md'))
.map(child => ({ uri: child.resource, storage: BUILTIN_STORAGE, type }));
} catch {
return [];
}
}
//#region Built-in Skills
/**
@@ -189,18 +149,17 @@ export class AgenticPromptsService extends PromptsService {
//#endregion
/**
* Override to include built-in prompts and built-in skills, filtering out
* those overridden by user or workspace items with the same name.
* Override to include built-in skills, filtering out those overridden by
* user or workspace items with the same name.
*/
public override async listPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IPromptPath[]> {
const baseResults = await super.listPromptFiles(type, token);
let builtinItems: readonly IBuiltinPromptPath[];
if (type === PromptsType.skill) {
builtinItems = await this.getBuiltinSkillPaths();
} else {
builtinItems = await this.getBuiltinPromptFiles(type);
if (type !== PromptsType.skill) {
return baseResults;
}
const builtinItems = await this.getBuiltinSkillPaths();
if (builtinItems.length === 0) {
return baseResults;
}
@@ -209,12 +168,12 @@ export class AgenticPromptsService extends PromptsService {
const overriddenNames = new Set<string>();
for (const p of baseResults) {
if (p.storage === PromptsStorage.local || p.storage === PromptsStorage.user) {
overriddenNames.add(type === PromptsType.skill ? basename(dirname(p.uri)) : getCleanPromptName(p.uri));
overriddenNames.add(basename(dirname(p.uri)));
}
}
const nonOverridden = builtinItems.filter(
p => !overriddenNames.has(type === PromptsType.skill ? basename(dirname(p.uri)) : getCleanPromptName(p.uri))
p => !overriddenNames.has(basename(dirname(p.uri)))
);
// Built-in items use BUILTIN_STORAGE ('builtin') which is not in the
// core IPromptPath union but is handled by the sessions UI layer.
@@ -226,7 +185,8 @@ export class AgenticPromptsService extends PromptsService {
if (type === PromptsType.skill) {
return this.getBuiltinSkillPaths() as Promise<readonly IPromptPath[]>;
}
return this.getBuiltinPromptFiles(type) as Promise<readonly IPromptPath[]>;
// Built-in storage is only valid for skills; for other types, there are no items.
return [];
}
return super.listPromptFilesForStorage(type, storage, token);
}

View File

@@ -1,7 +1,10 @@
---
description: Act on user feedback attached to the current session
name: act-on-feedback
description: Act on user feedback attached to the current session. Use when the user submits feedback on the session's changes via the Submit Feedback button.
---
<!-- Customize this prompt and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
<!-- Customize this skill and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
# Act on Feedback
The user has provided feedback on the current session's changes. Their feedback comments have been attached to this message.

View File

@@ -1,7 +1,10 @@
---
description: Create a draft pull request for the current session
name: create-draft-pr
description: Create a draft pull request for the current session. Use when the user wants to open a draft PR with the session's changes.
---
<!-- Customize this prompt and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
<!-- Customize this skill and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
# Create Draft Pull Request
Use the GitHub MCP server to create a draft pull request — do NOT use the `gh` CLI.

View File

@@ -1,7 +1,10 @@
---
description: Create a pull request for the current session
name: create-pr
description: Create a pull request for the current session. Use when the user wants to open a PR with the session's changes.
---
<!-- Customize this prompt and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
<!-- Customize this skill and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
# Create Pull Request
Use the GitHub MCP server to create a pull request — do NOT use the `gh` CLI.

View File

@@ -1,7 +1,10 @@
---
description: Generate or modify run commands for the current session
name: generate-run-commands
description: Generate or modify run commands for the current session. Use when the user wants to set up or update run commands that appear in the session's Run button.
---
<!-- Customize this prompt and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
<!-- Customize this skill and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
# Generate Run Commands
Help the user set up run commands for the current Agent Session workspace. Run commands appear in the session's Run button in the title bar.

View File

@@ -1,7 +1,10 @@
---
description: Merge changes from the topic branch to the merge base branch
name: merge-changes
description: Merge changes from the topic branch to the merge base branch. Use when the user wants to merge their session's work back to the base branch.
---
<!-- Customize this prompt and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
<!-- Customize this skill and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
# Merge Changes
Merge changes from the topic branch to the merge base branch.
The context block appended to the prompt contains the source and target branch information.

View File

@@ -1,7 +1,10 @@
---
description: Update the pull request for the current session
name: update-pr
description: Update the pull request for the current session. Use when the user wants to push new changes to an existing PR.
---
<!-- Customize this prompt and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
<!-- Customize this skill and select save to override its behavior. Delete that copy to restore the built-in behavior. -->
# Update Pull Request
Update the existing pull request for the current session.
The context block appended to the prompt contains the pull request information.

View File

@@ -1293,11 +1293,14 @@ export class AICustomizationListWidget extends Disposable {
extensionInfoByUri.set(file.uri.toString(), { id: file.extension.identifier, displayName: file.extension.displayName });
}
}
const uiIntegrations = this.workspaceService.getSkillUIIntegrations();
const seenUris = new ResourceSet();
for (const skill of skills || []) {
const filename = basename(skill.uri);
const skillName = skill.name || basename(dirname(skill.uri)) || filename;
seenUris.add(skill.uri);
const skillFolderName = basename(dirname(skill.uri));
const uiTooltip = uiIntegrations.get(skillFolderName);
items.push({
id: skill.uri.toString(),
uri: skill.uri,
@@ -1308,6 +1311,8 @@ export class AICustomizationListWidget extends Disposable {
promptType,
pluginUri: skill.storage === PromptsStorage.plugin ? this.findPluginUri(skill.uri) : undefined,
disabled: false,
badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined,
badgeTooltip: uiTooltip,
});
}
// Also include disabled skills from the raw file list
@@ -1315,15 +1320,20 @@ export class AICustomizationListWidget extends Disposable {
for (const file of allSkillFiles) {
if (!seenUris.has(file.uri) && disabledUris.has(file.uri)) {
const filename = basename(file.uri);
const disabledName = file.name || basename(dirname(file.uri)) || filename;
const disabledFolderName = basename(dirname(file.uri));
const uiTooltip = uiIntegrations.get(disabledFolderName);
items.push({
id: file.uri.toString(),
uri: file.uri,
name: file.name || basename(dirname(file.uri)) || filename,
name: disabledName,
filename,
description: file.description,
storage: file.storage,
promptType,
disabled: true,
badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined,
badgeTooltip: uiTooltip,
});
}
}

View File

@@ -93,6 +93,12 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic
async getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
return this.promptsService.getPromptSlashCommands(token);
}
private static readonly _emptyIntegrations: ReadonlyMap<string, string> = new Map();
getSkillUIIntegrations(): ReadonlyMap<string, string> {
return AICustomizationWorkspaceService._emptyIntegrations;
}
}
registerSingleton(IAICustomizationWorkspaceService, AICustomizationWorkspaceService, InstantiationType.Delayed);

View File

@@ -150,4 +150,13 @@ export interface IAICustomizationWorkspaceService {
* customizations visible in the AI Customization views.
*/
getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]>;
/**
* Returns a map of built-in skill names that have direct UI integrations
* (toolbar buttons, menu items, etc.) to a tooltip describing the
* integration. Used to display a 'UI Integration' badge in the
* customizations editor, especially important when users override a
* built-in skill that drives a UI surface.
*/
getSkillUIIntegrations(): ReadonlyMap<string, string>;
}

View File

@@ -290,6 +290,11 @@ const allFiles: IFixtureFile[] = [
// Skills - extension (built-in + third-party)
{ uri: URI.file('/extensions/github.copilot-chat/skills/workspace/SKILL.md'), storage: PromptsStorage.extension, type: PromptsType.skill, name: 'Workspace Search', description: 'Built-in workspace search skill', extensionId: 'GitHub.copilot-chat', extensionDisplayName: 'GitHub Copilot Chat' },
{ uri: URI.file('/extensions/acme.tools/skills/audit/SKILL.md'), storage: PromptsStorage.extension, type: PromptsType.skill, name: 'Audit', description: 'Third-party audit skill', extensionId: 'acme.tools', extensionDisplayName: 'Acme Tools' },
// Skills - built-in (sessions bundled skills with UI integrations)
{ uri: URI.file('/app/skills/act-on-feedback/SKILL.md'), storage: BUILTIN_STORAGE as PromptsStorage, type: PromptsType.skill, name: 'act-on-feedback', description: 'Act on user feedback attached to the current session' },
{ uri: URI.file('/app/skills/generate-run-commands/SKILL.md'), storage: BUILTIN_STORAGE as PromptsStorage, type: PromptsType.skill, name: 'generate-run-commands', description: 'Generate or modify run commands for the current session' },
{ uri: URI.file('/app/skills/commit/SKILL.md'), storage: BUILTIN_STORAGE as PromptsStorage, type: PromptsType.skill, name: 'commit', description: 'Commit staged or unstaged changes with an AI-generated commit message' },
{ uri: URI.file('/app/skills/create-pr/SKILL.md'), storage: BUILTIN_STORAGE as PromptsStorage, type: PromptsType.skill, name: 'create-pr', description: 'Create a pull request for the current session' },
// Prompts — workspace
{ uri: URI.file('/workspace/.github/prompts/explain.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Explain', description: 'Explain selected code' },
{ uri: URI.file('/workspace/.github/prompts/review.prompt.md'), storage: PromptsStorage.local, type: PromptsType.prompt, name: 'Review', description: 'Review changes' },
@@ -353,6 +358,7 @@ interface IRenderEditorOptions {
readonly scrollToBottom?: boolean;
readonly width?: number;
readonly height?: number;
readonly skillUIIntegrations?: ReadonlyMap<string, string>;
}
async function waitForAnimationFrames(count: number): Promise<void> {
@@ -423,6 +429,7 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor
ctx.container.style.height = `${height}px`;
const isSessionsWindow = options.isSessionsWindow ?? false;
const skillUIIntegrations = options.skillUIIntegrations ?? new Map();
const managementSections = options.managementSections ?? [
AICustomizationManagementSection.Agents,
AICustomizationManagementSection.Skills,
@@ -470,12 +477,14 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor
override setOverrideProjectRoot() { }
override readonly managementSections = managementSections;
override async generateCustomization() { }
override getSkillUIIntegrations() { return skillUIIntegrations; }
}());
reg.defineInstance(ICustomizationHarnessService, harnessService);
reg.defineInstance(IChatSessionsService, new class extends mock<IChatSessionsService>() {
override readonly onDidChangeCustomizations = Event.None;
override async getCustomizations() { return undefined; }
override getRegisteredChatSessionItemProviders() { return []; }
override hasCustomizationsProvider() { return false; }
}());
reg.defineInstance(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {
override readonly onDidChangeWorkspaceFolders = Event.None;
@@ -812,6 +821,32 @@ export default defineThemedFixtureGroup({ path: 'chat/aiCustomizations/' }, {
}),
}),
// Sessions Skills tab showing UI Integration badges on built-in skills
SessionsSkillsTab: defineComponentFixture({
labels: { kind: 'screenshot' },
render: ctx => renderEditor(ctx, {
harness: CustomizationHarness.CLI,
isSessionsWindow: true,
selectedSection: AICustomizationManagementSection.Skills,
availableHarnesses: [
createCliHarnessDescriptor(getCliUserRoots(userHome), [BUILTIN_STORAGE]),
],
managementSections: [
AICustomizationManagementSection.Agents,
AICustomizationManagementSection.Skills,
AICustomizationManagementSection.Instructions,
AICustomizationManagementSection.Prompts,
AICustomizationManagementSection.Hooks,
AICustomizationManagementSection.McpServers,
AICustomizationManagementSection.Plugins,
],
skillUIIntegrations: new Map([
['act-on-feedback', 'Used by the Submit Feedback button in the Changes toolbar'],
['generate-run-commands', 'Used by the Run button in the title bar'],
]),
}),
}),
// MCP Servers tab with many servers to verify scrollable list layout
McpServersTab: defineComponentFixture({
labels: { kind: 'screenshot', blocksCi: true },