From 7e1bdc2ac447fe66d7a92e71bb838fd2b57f0979 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:26:45 -0800 Subject: [PATCH 01/10] sessions/customizations: better control over counts (#297876) --- .../aiCustomizationWorkspaceService.ts | 7 +++++++ .../sessions/browser/customizationCounts.ts | 14 ++++++------- .../customizationsToolbar.contribution.ts | 21 +++++++++++++------ .../aiCustomizationListWidget.ts | 3 ++- .../aiCustomizationWorkspaceService.ts | 4 ++++ .../common/aiCustomizationWorkspaceService.ts | 6 ++++++ 6 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index bfb415f522f33..2bb4fe99b64e6 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -57,6 +57,13 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization PromptsStorage.user, ]; + getVisibleStorageSources(type: PromptsType): readonly PromptsStorage[] { + if (type === PromptsType.hook) { + return [PromptsStorage.local]; + } + return this.visibleStorageSources; + } + readonly preferManualCreation = true; async commitFiles(projectRoot: URI, fileUris: URI[]): Promise { diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index ce46e82314f23..98015af7a0ab0 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -24,9 +24,9 @@ const storageToCountKey: Partial> = [PromptsStorage.extension]: 'extension', }; -export function getSourceCountsTotal(counts: ISourceCounts, workspaceService: IAICustomizationWorkspaceService): number { +export function getSourceCountsTotal(counts: ISourceCounts, workspaceService: IAICustomizationWorkspaceService, type: PromptsType): number { let total = 0; - for (const storage of workspaceService.visibleStorageSources) { + for (const storage of workspaceService.getVisibleStorageSources(type)) { const key = storageToCountKey[storage]; if (key) { total += counts[key]; @@ -84,10 +84,10 @@ export async function getCustomizationTotalCount(promptsService: IPromptsService getPromptSourceCounts(promptsService, PromptsType.hook, excluded), ]); - return getSourceCountsTotal(agentCounts, workspaceService) - + getSourceCountsTotal(skillCounts, workspaceService) - + getSourceCountsTotal(instructionCounts, workspaceService) - + getSourceCountsTotal(promptCounts, workspaceService) - + getSourceCountsTotal(hookCounts, workspaceService) + return getSourceCountsTotal(agentCounts, workspaceService, PromptsType.agent) + + getSourceCountsTotal(skillCounts, workspaceService, PromptsType.skill) + + getSourceCountsTotal(instructionCounts, workspaceService, PromptsType.instructions) + + getSourceCountsTotal(promptCounts, workspaceService, PromptsType.prompt) + + getSourceCountsTotal(hookCounts, workspaceService, PromptsType.hook) + mcpService.servers.get().length; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index 23398d75287f9..827d162f79470 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -15,7 +15,7 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; -import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; +import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js'; import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; @@ -40,6 +40,7 @@ interface ICustomizationItemConfig { readonly label: string; readonly icon: ThemeIcon; readonly section: AICustomizationManagementSection; + readonly promptType?: PromptsType; readonly getSourceCounts?: (promptsService: IPromptsService, excludedUserFileRoots: readonly URI[]) => Promise; readonly getCount?: (languageModelsService: ILanguageModelsService, mcpService: IMcpService) => Promise; } @@ -50,6 +51,7 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ label: localize('agents', "Agents"), icon: agentIcon, section: AICustomizationManagementSection.Agents, + promptType: PromptsType.agent, getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.agent, ex), }, { @@ -57,6 +59,7 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ label: localize('skills', "Skills"), icon: skillIcon, section: AICustomizationManagementSection.Skills, + promptType: PromptsType.skill, getSourceCounts: (ps, ex) => getSkillSourceCounts(ps, ex), }, { @@ -64,6 +67,7 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ label: localize('instructions', "Instructions"), icon: instructionsIcon, section: AICustomizationManagementSection.Instructions, + promptType: PromptsType.instructions, getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.instructions, ex), }, { @@ -71,6 +75,7 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ label: localize('prompts', "Prompts"), icon: promptIcon, section: AICustomizationManagementSection.Prompts, + promptType: PromptsType.prompt, getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.prompt, ex), }, { @@ -78,6 +83,7 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ label: localize('hooks', "Hooks"), icon: hookIcon, section: AICustomizationManagementSection.Hooks, + promptType: PromptsType.hook, getSourceCounts: (ps, ex) => getPromptSourceCounts(ps, PromptsType.hook, ex), }, // TODO: Re-enable MCP Servers once CLI MCP configuration is unified with VS Code @@ -172,19 +178,22 @@ class CustomizationLinkViewItem extends ActionViewItem { private _renderSourceCounts(container: HTMLElement, counts: ISourceCounts): void { container.textContent = ''; - const total = getSourceCountsTotal(counts, this._workspaceService); + const type = this._config.promptType; + const visibleSources = type ? this._workspaceService.getVisibleStorageSources(type) : this._workspaceService.visibleStorageSources; + const total = getSourceCountsTotal(counts, this._workspaceService, type ?? PromptsType.prompt); container.classList.toggle('hidden', total === 0); if (total === 0) { return; } - const sources: { count: number; icon: ThemeIcon; title: string }[] = [ - { count: counts.workspace, icon: workspaceIcon, title: localize('workspaceCount', "{0} from workspace", counts.workspace) }, - { count: counts.user, icon: userIcon, title: localize('userCount', "{0} from user", counts.user) }, + const visibleSourcesSet = new Set(visibleSources); + const sources: { storage: PromptsStorage; count: number; icon: ThemeIcon; title: string }[] = [ + { storage: PromptsStorage.local, count: counts.workspace, icon: workspaceIcon, title: localize('workspaceCount', "{0} from workspace", counts.workspace) }, + { storage: PromptsStorage.user, count: counts.user, icon: userIcon, title: localize('userCount', "{0} from user", counts.user) }, ]; for (const source of sources) { - if (source.count === 0) { + if (source.count === 0 || !visibleSourcesSet.has(source.storage)) { continue; } const badge = append(container, $('span.source-count-badge')); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 5f52802bda9cb..c53f14d8b16a3 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -964,7 +964,8 @@ export class AICustomizationListWidget extends Disposable { this.logService.info(`[AICustomizationListWidget] filterItems: allItems=${this.allItems.length}, matched=${totalBeforeFilter}`); // Group items by storage - const visibleSources = new Set(this.workspaceService.visibleStorageSources); + const promptType = sectionToPromptType(this.currentSection); + const visibleSources = new Set(this.workspaceService.getVisibleStorageSources(promptType)); const groups: { storage: PromptsStorage; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [ { storage: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, { storage: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 4f4828681261b..07684fb6a1084 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -60,6 +60,10 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic PromptsStorage.plugin, ]; + getVisibleStorageSources(_type: PromptsType): readonly PromptsStorage[] { + return this.visibleStorageSources; + } + readonly preferManualCreation = false; readonly excludedUserFileRoots: readonly URI[] = []; diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index c404cb281ab1d..93de4ba4d710d 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -52,6 +52,12 @@ export interface IAICustomizationWorkspaceService { */ readonly visibleStorageSources: readonly PromptsStorage[]; + /** + * Returns the visible storage sources for a specific customization type. + * Allows per-type overrides (e.g., hooks may only show workspace sources). + */ + getVisibleStorageSources(type: PromptsType): readonly PromptsStorage[]; + /** * URI roots to exclude from user-level file listings. * Files under these roots are hidden from the customization list. From a69b4ae8840824e152fd0cb1328c175b5e2795c8 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Wed, 25 Feb 2026 22:00:07 -0800 Subject: [PATCH 02/10] feat(chat): add experimental icons for new chat button (#297875) * feat(chat): add experimental icons for new chat button * resolve comments * :lipstick: --- .../chat/browser/actions/chatActions.ts | 50 ++++++++++++++++++- .../contrib/chat/browser/chat.contribution.ts | 16 ++++++ .../chat/common/actions/chatContextKeys.ts | 2 + 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 4903908dd4997..e881fc4c33631 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -679,7 +679,55 @@ export function registerChatActions() { }, { id: MenuId.EditorTitle, group: 'navigation', - when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('copilot'), ChatContextKeys.newChatButtonExperimentIcon.notEqualsTo('sparkle')), + order: 1 + }], + }); + } + + async run(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + await widgetService.openSession(LocalChatSessionUri.getNewSessionUri(), ACTIVE_GROUP, { pinned: true } satisfies IChatEditorOptions); + } + }); + + registerAction2(class NewChatEditorCopilotIconAction extends Action2 { + constructor() { + super({ + id: ACTION_ID_OPEN_CHAT + '.copilotIcon', + title: localize2('interactiveSession.open', "New Chat Editor"), + icon: Codicon.copilot, + f1: false, + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.isEqualTo('copilot')), + order: 1 + }], + }); + } + + async run(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + await widgetService.openSession(LocalChatSessionUri.getNewSessionUri(), ACTIVE_GROUP, { pinned: true } satisfies IChatEditorOptions); + } + }); + + registerAction2(class NewChatEditorSparkleIconAction extends Action2 { + constructor() { + super({ + id: ACTION_ID_OPEN_CHAT + '.sparkleIcon', + title: localize2('interactiveSession.open', "New Chat Editor"), + icon: Codicon.chatSparkle, + f1: false, + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled, + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), ChatContextKeys.newChatButtonExperimentIcon.isEqualTo('sparkle')), order: 1 }], }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d8b6675e6cf0c..8a10791bf5045 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -16,6 +16,7 @@ import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurati import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { McpAccessValue, McpAutoStartValue, mcpAccessConfig, mcpAutoStartConfig, mcpGalleryServiceEnablementConfig, mcpGalleryServiceUrlConfig, mcpAppsEnabledConfig } from '../../../../platform/mcp/common/mcpManagement.js'; import product from '../../../../platform/product/common/product.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; @@ -97,6 +98,7 @@ import { PromptsDebugContribution } from './promptsDebugContribution.js'; import { ChatDebugEditorInput, ChatDebugEditorInputSerializer } from './chatDebug/chatDebugEditorInput.js'; import './agentSessions/agentSessions.contribution.js'; import { backgroundAgentDisplayName } from './agentSessions/agentSessions.js'; +import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from './chat.js'; import { ChatAccessibilityService } from './accessibility/chatAccessibilityService.js'; @@ -1409,14 +1411,18 @@ class ChatDebugResolverContribution implements IWorkbenchContribution { class ChatAgentSettingContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatAgentSetting'; + private readonly newChatButtonExperimentIcon; constructor( @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @IChatEntitlementService private readonly entitlementService: IChatEntitlementService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); + this.newChatButtonExperimentIcon = ChatContextKeys.newChatButtonExperimentIcon.bindTo(this.contextKeyService); this.registerMaxRequestsSetting(); this.registerBackgroundAgentDisplayName(); + this.registerNewChatButtonIcon(); } @@ -1455,6 +1461,16 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr } }); } + + private registerNewChatButtonIcon(): void { + this.experimentService.getTreatment('chatNewButtonIcon').then((value) => { + if (value === 'copilot' || value === 'sparkle') { + this.newChatButtonExperimentIcon.set(value); + } else { + this.newChatButtonExperimentIcon.reset(); + } + }); + } } diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 9ab62c99bbf22..d7e4763aa4d76 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -140,6 +140,8 @@ export namespace ChatContextKeys { export const hasUsedCreateSlashCommands = new RawContextKey('chatHasUsedCreateSlashCommands', false, { type: 'boolean', description: localize('chatHasUsedCreateSlashCommands', "True when the user has used any of the /create-* slash commands.") }); export const contextUsageHasBeenOpened = new RawContextKey('chatContextUsageHasBeenOpened', false, { type: 'boolean', description: localize('chatContextUsageHasBeenOpened', "True when the user has opened the context window usage details.") }); + + export const newChatButtonExperimentIcon = new RawContextKey('chatNewChatButtonExperimentIcon', '', { type: 'string', description: localize('chatNewChatButtonExperimentIcon', "The icon variant for the new chat button, controlled by experiment. Values: 'copilot', 'sparkle', or empty for default.") }); } export namespace ChatContextKeyExprs { From 38172ba85d6ac47106f482a1f1e71f07c570ed76 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 25 Feb 2026 22:45:09 -0800 Subject: [PATCH 03/10] Improve hooks linting support (#297852) --- .../contrib/chat/browser/chat.contribution.ts | 52 ++++- .../promptSyntax/hookCopilotCliCompat.ts | 15 +- .../chat/common/promptSyntax/hookSchema.ts | 181 ++++++++++++++---- 3 files changed, 197 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 8a10791bf5045..6bc7c2d60dfd4 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { PolicyCategory } from '../../../../base/common/policy.js'; @@ -53,10 +53,11 @@ import { ILanguageModelToolsService } from '../common/tools/languageModelToolsSe import { agentPluginDiscoveryRegistry, IAgentPluginService } from '../common/plugins/agentPluginService.js'; import { ChatPromptFilesExtensionPointHandler } from '../common/promptSyntax/chatPromptFilesContribution.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; -import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, CLAUDE_AGENTS_SOURCE_FOLDER, DEFAULT_HOOK_FILE_PATHS, DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS } from '../common/promptSyntax/config/promptFileLocations.js'; +import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, CLAUDE_AGENTS_SOURCE_FOLDER, DEFAULT_HOOK_FILE_PATHS, DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS, PromptFileSource } from '../common/promptSyntax/config/promptFileLocations.js'; import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js'; -import { AGENT_DOCUMENTATION_URL, INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL } from '../common/promptSyntax/promptTypes.js'; -import { hookFileSchema, HOOK_SCHEMA_URI, HOOK_FILE_GLOB } from '../common/promptSyntax/hookSchema.js'; +import { AGENT_DOCUMENTATION_URL, INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL, PromptsType } from '../common/promptSyntax/promptTypes.js'; +import { hookFileSchema, HOOK_SCHEMA_URI } from '../common/promptSyntax/hookSchema.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { IPromptsService } from '../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../common/promptSyntax/service/promptsServiceImpl.js'; @@ -171,7 +172,6 @@ const toolReferenceNameEnumDescriptions: string[] = []; // Register JSON schema for hook files const jsonContributionRegistry = Registry.as(JSONExtensions.JSONContribution); jsonContributionRegistry.registerSchema(HOOK_SCHEMA_URI, hookFileSchema); -jsonContributionRegistry.registerSchemaAssociation(HOOK_SCHEMA_URI, HOOK_FILE_GLOB); // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -1560,6 +1560,47 @@ class ChatAgentActionsContribution extends Disposable implements IWorkbenchContr } } +class HookSchemaAssociationContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.hookSchemaAssociation'; + + private readonly _registrations = this._register(new DisposableStore()); + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + this._updateAssociations(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(PromptsConfig.HOOKS_LOCATION_KEY)) { + this._updateAssociations(); + } + })); + } + + private _updateAssociations(): void { + this._registrations.clear(); + + const folders = PromptsConfig.promptSourceFolders(this._configurationService, PromptsType.hook); + + for (const folder of folders) { + // Skip Claude settings files — they use a different schema format + if (folder.source === PromptFileSource.ClaudeWorkspace || folder.source === PromptFileSource.ClaudeWorkspaceLocal || folder.source === PromptFileSource.ClaudePersonal) { + continue; + } + + // If it's a specific .json file, use it directly; otherwise treat as directory + const glob = folder.path.toLowerCase().endsWith('.json') + ? folder.path + : `${folder.path}/*.json`; + + this._registrations.add( + jsonContributionRegistry.registerSchemaAssociation(HOOK_SCHEMA_URI, glob) + ); + } + } +} + class ToolReferenceNamesContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.toolReferenceNames'; @@ -1630,6 +1671,7 @@ registerWorkbenchContribution2(UsagesToolContribution.ID, UsagesToolContribution registerWorkbenchContribution2(RenameToolContribution.ID, RenameToolContribution, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatAgentActionsContribution.ID, ChatAgentActionsContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(HookSchemaAssociationContribution.ID, HookSchemaAssociationContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ToolReferenceNamesContribution.ID, ToolReferenceNamesContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatAgentRecommendation.ID, ChatAgentRecommendation, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEditorAccessibility, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts index 16ca9a825ae81..587771544b28d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCopilotCliCompat.ts @@ -3,18 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { HookType } from './hookSchema.js'; - -/** - * Maps Copilot CLI hook type names to our abstract HookType. - * Copilot CLI uses camelCase names. - */ -export const COPILOT_CLI_HOOK_TYPE_MAP: Record = { - 'sessionStart': HookType.SessionStart, - 'userPromptSubmitted': HookType.UserPromptSubmit, - 'preToolUse': HookType.PreToolUse, - 'postToolUse': HookType.PostToolUse, -}; +import { COPILOT_CLI_HOOK_TYPE_MAP, HookType } from './hookSchema.js'; /** * Cached inverse mapping from HookType to Copilot CLI hook type name. @@ -36,7 +25,7 @@ function getHookTypeToCopilotCliNameMap(): Map { * Resolves a Copilot CLI hook type name to our abstract HookType. */ export function resolveCopilotCliHookType(name: string): HookType | undefined { - return COPILOT_CLI_HOOK_TYPE_MAP[name]; + return (COPILOT_CLI_HOOK_TYPE_MAP as Record)[name]; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts index ccf368ed54a06..fcbbdfea8207e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts @@ -25,6 +25,17 @@ export enum HookType { Stop = 'Stop', } +/** + * Maps Copilot CLI hook type names to our abstract HookType. + * Copilot CLI uses camelCase names. + */ +export const COPILOT_CLI_HOOK_TYPE_MAP = { + 'sessionStart': HookType.SessionStart, + 'userPromptSubmitted': HookType.UserPromptSubmit, + 'preToolUse': HookType.PreToolUse, + 'postToolUse': HookType.PostToolUse, +} as const satisfies Record; + /** * String literal type derived from HookType enum values. */ @@ -177,6 +188,116 @@ const hookArraySchema: IJSONSchema = { items: hookCommandSchema }; +/** + * Hook properties for the VS Code / PascalCase format. + */ +const vscodeHookProperties: { [key in HookType]: IJSONSchema } = { + SessionStart: { + ...hookArraySchema, + description: nls.localize('hookFile.sessionStart', 'Executed when a new agent session begins. Use to initialize environments, log session starts, validate project state, or set up temporary resources.') + }, + UserPromptSubmit: { + ...hookArraySchema, + description: nls.localize('hookFile.userPromptSubmit', 'Executed when the user submits a prompt to the agent. Use to log user requests for auditing and usage analysis.') + }, + PreToolUse: { + ...hookArraySchema, + description: nls.localize('hookFile.preToolUse', 'Executed before the agent uses any tool. This is the most powerful hook as it can approve or deny tool executions. Use to block dangerous commands, enforce security policies, require approval for sensitive operations, or log tool usage.') + }, + PostToolUse: { + ...hookArraySchema, + description: nls.localize('hookFile.postToolUse', 'Executed after a tool completes execution successfully. Use to log execution results, track usage statistics, generate audit trails, or monitor performance.') + }, + PreCompact: { + ...hookArraySchema, + description: nls.localize('hookFile.preCompact', 'Executed before the agent compacts the conversation context. Use to save conversation state, export important information, or prepare for context reduction.') + }, + SubagentStart: { + ...hookArraySchema, + description: nls.localize('hookFile.subagentStart', 'Executed when a subagent is started. Use to log subagent spawning, track nested agent usage, or initialize subagent-specific resources.') + }, + SubagentStop: { + ...hookArraySchema, + description: nls.localize('hookFile.subagentStop', 'Executed when a subagent stops. Use to log subagent completion, cleanup subagent resources, or aggregate subagent results.') + }, + Stop: { + ...hookArraySchema, + description: nls.localize('hookFile.stop', 'Executed when the agent session stops. Use to cleanup resources, generate final reports, or send completion notifications.') + } +}; + +/** + * Hook command schema for the Copilot CLI format. + * Adds `bash`, `powershell`, and `timeoutSec` fields alongside the standard ones. + */ +const copilotCliHookCommandSchema: IJSONSchema = { + type: 'object', + additionalProperties: true, + required: ['type'], + anyOf: [ + { required: ['bash'] }, + { required: ['powershell'] } + ], + errorMessage: nls.localize('hook.cliCommandRequired', 'At least one of "bash" or "powershell" must be specified.'), + properties: { + type: { + type: 'string', + enum: ['command'], + description: nls.localize('hook.type', 'Must be "command".') + }, + bash: { + type: 'string', + description: nls.localize('hook.bash', 'Bash command for Linux and macOS.') + }, + powershell: { + type: 'string', + description: nls.localize('hook.powershell', 'PowerShell command for Windows.') + }, + cwd: { + type: 'string', + description: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).') + }, + env: { + type: 'object', + additionalProperties: { type: 'string' }, + description: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.') + }, + timeoutSec: { + type: 'number', + default: 10, + description: nls.localize('hook.timeoutSec', 'Maximum execution time in seconds (default: 10).') + } + } +}; + +const copilotCliHookArraySchema: IJSONSchema = { + type: 'array', + items: copilotCliHookCommandSchema +}; + +/** + * Hook properties for the Copilot CLI / camelCase format. + * Maps from the Copilot CLI hook type names defined in COPILOT_CLI_HOOK_TYPE_MAP. + */ +const copilotCliHookProperties: { [key in keyof typeof COPILOT_CLI_HOOK_TYPE_MAP]: IJSONSchema } = { + sessionStart: { + ...copilotCliHookArraySchema, + description: nls.localize('hookFile.cli.sessionStart', 'Executed when a new agent session begins.') + }, + userPromptSubmitted: { + ...copilotCliHookArraySchema, + description: nls.localize('hookFile.cli.userPromptSubmitted', 'Executed when the user submits a prompt to the agent.') + }, + preToolUse: { + ...copilotCliHookArraySchema, + description: nls.localize('hookFile.cli.preToolUse', 'Executed before the agent uses any tool. Can approve or deny tool executions.') + }, + postToolUse: { + ...copilotCliHookArraySchema, + description: nls.localize('hookFile.cli.postToolUse', 'Executed after a tool completes execution successfully.') + }, +}; + export const hookFileSchema: IJSONSchema = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', @@ -188,39 +309,33 @@ export const hookFileSchema: IJSONSchema = { type: 'object', description: nls.localize('hookFile.hooks', 'Hook definitions organized by type.'), additionalProperties: true, - properties: { - SessionStart: { - ...hookArraySchema, - description: nls.localize('hookFile.sessionStart', 'Executed when a new agent session begins. Use to initialize environments, log session starts, validate project state, or set up temporary resources.') - }, - UserPromptSubmit: { - ...hookArraySchema, - description: nls.localize('hookFile.userPromptSubmit', 'Executed when the user submits a prompt to the agent. Use to log user requests for auditing and usage analysis.') - }, - PreToolUse: { - ...hookArraySchema, - description: nls.localize('hookFile.preToolUse', 'Executed before the agent uses any tool. This is the most powerful hook as it can approve or deny tool executions. Use to block dangerous commands, enforce security policies, require approval for sensitive operations, or log tool usage.') - }, - PostToolUse: { - ...hookArraySchema, - description: nls.localize('hookFile.postToolUse', 'Executed after a tool completes execution successfully. Use to log execution results, track usage statistics, generate audit trails, or monitor performance.') - }, - PreCompact: { - ...hookArraySchema, - description: nls.localize('hookFile.preCompact', 'Executed before the agent compacts the conversation context. Use to save conversation state, export important information, or prepare for context reduction.') - }, - SubagentStart: { - ...hookArraySchema, - description: nls.localize('hookFile.subagentStart', 'Executed when a subagent is started. Use to log subagent spawning, track nested agent usage, or initialize subagent-specific resources.') - }, - SubagentStop: { - ...hookArraySchema, - description: nls.localize('hookFile.subagentStop', 'Executed when a subagent stops. Use to log subagent completion, cleanup subagent resources, or aggregate subagent results.') - }, - Stop: { - ...hookArraySchema, - description: nls.localize('hookFile.stop', 'Executed when the agent session stops. Use to cleanup resources, generate final reports, or send completion notifications.') - } + } + }, + // Conditionally apply PascalCase or camelCase hook properties based on + // whether the file uses the Copilot CLI format (detected by the "version" field). + if: { + required: ['version'], + properties: { + version: { type: 'number' } + } + }, + then: { + // Copilot CLI format: camelCase hook names, bash/powershell/timeoutSec fields + properties: { + version: { + type: 'number', + description: nls.localize('hookFile.version', 'Hook configuration format version.'), + }, + hooks: { + properties: copilotCliHookProperties + } + } + }, + else: { + // VS Code / PascalCase format + properties: { + hooks: { + properties: vscodeHookProperties } } }, From 8f0892c678a24ad0ae49f46c8e2e76b0171b80fd Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:45:13 -0800 Subject: [PATCH 04/10] sessions: force creating a "github cli"-compatible hooks file (#297881) * sessions: force creating a "github cli"-compatible hooks file * seed with events --- .../aiCustomizationListWidget.ts | 113 ++++++++++-------- .../aiCustomizationManagementEditor.ts | 65 ++++++++-- 2 files changed, 120 insertions(+), 58 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index c53f14d8b16a3..4d240a6a64a6e 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -28,17 +28,12 @@ import { IContextMenuService, IContextViewService } from '../../../../../platfor import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { matchesContiguousSubString, IMatch } from '../../../../../base/common/filters.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js'; +import { Button, ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js'; import { IMenuService } from '../../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IPathService } from '../../../../services/path/common/pathService.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { parseAllHookFiles } from '../promptSyntax/hookUtils.js'; -import { OS } from '../../../../../base/common/platform.js'; -import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { Action, Separator } from '../../../../../base/common/actions.js'; @@ -327,7 +322,9 @@ export class AICustomizationListWidget extends Disposable { private searchAndButtonContainer!: HTMLElement; private searchContainer!: HTMLElement; private searchInput!: InputBox; + private addButtonContainer!: HTMLElement; private addButton!: ButtonWithDropdown; + private addButtonSimple!: Button; private listContainer!: HTMLElement; private list!: WorkbenchList; private emptyStateContainer!: HTMLElement; @@ -364,11 +361,8 @@ export class AICustomizationListWidget extends Disposable { @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IFileService private readonly fileService: IFileService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IPathService private readonly pathService: IPathService, @ILabelService private readonly labelService: ILabelService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @ILogService private readonly logService: ILogService, @IClipboardService private readonly clipboardService: IClipboardService, @@ -416,9 +410,19 @@ export class AICustomizationListWidget extends Disposable { this.delayedFilter.trigger(() => this.filterItems()); })); - // Add button with dropdown next to search - const addButtonContainer = DOM.append(this.searchAndButtonContainer, $('.list-add-button-container')); - this.addButton = this._register(new ButtonWithDropdown(addButtonContainer, { + // Add button container next to search + this.addButtonContainer = DOM.append(this.searchAndButtonContainer, $('.list-add-button-container')); + + // Simple button (for single-action case, no dropdown) + this.addButtonSimple = this._register(new Button(this.addButtonContainer, { + ...defaultButtonStyles, + supportIcons: true, + })); + this.addButtonSimple.element.classList.add('list-add-button'); + this._register(this.addButtonSimple.onDidClick(() => this.executePrimaryCreateAction())); + + // Button with dropdown (for multi-action case) + this.addButton = this._register(new ButtonWithDropdown(this.addButtonContainer, { ...defaultButtonStyles, supportIcons: true, contextMenuProvider: this.contextMenuService, @@ -610,22 +614,50 @@ export class AICustomizationListWidget extends Disposable { */ private updateAddButton(): void { const typeLabel = this.getTypeLabel(); - this.addButton.primaryButton.setTitle(''); - this.addButton.dropdownButton.setTitle(''); - this.addButton.enabled = true; + const dropdownActions = this.getDropdownActions(); + const hasDropdown = dropdownActions.length > 0; + + // Toggle which button is visible + this.addButton.element.style.display = hasDropdown ? '' : 'none'; + this.addButtonSimple.element.style.display = hasDropdown ? 'none' : ''; + if (this.workspaceService.preferManualCreation) { // Sessions: primary is workspace creation const hasWorkspace = this.hasActiveWorkspace(); - this.addButton.label = `$(${Codicon.add.id}) New ${typeLabel} (Workspace)`; - this.addButton.enabled = hasWorkspace; - if (!hasWorkspace) { - const disabledTitle = localize('createDisabled', "Open a workspace folder to create customizations."); - this.addButton.primaryButton.setTitle(disabledTitle); - this.addButton.dropdownButton.setTitle(disabledTitle); + const label = `$(${Codicon.add.id}) New ${typeLabel} (Workspace)`; + + if (hasDropdown) { + this.addButton.label = label; + this.addButton.enabled = hasWorkspace; + this.addButton.primaryButton.setTitle(''); + this.addButton.dropdownButton.setTitle(''); + if (!hasWorkspace) { + const disabledTitle = localize('createDisabled', "Open a workspace folder to create customizations."); + this.addButton.primaryButton.setTitle(disabledTitle); + this.addButton.dropdownButton.setTitle(disabledTitle); + } + } else { + this.addButtonSimple.label = label; + this.addButtonSimple.enabled = hasWorkspace; + if (!hasWorkspace) { + this.addButtonSimple.setTitle(localize('createDisabled', "Open a workspace folder to create customizations.")); + } else { + this.addButtonSimple.setTitle(''); + } } } else { // Core: primary is AI generation - this.addButton.label = `$(${Codicon.sparkle.id}) Generate ${typeLabel}`; + const label = `$(${Codicon.sparkle.id}) Generate ${typeLabel}`; + if (hasDropdown) { + this.addButton.label = label; + this.addButton.enabled = true; + this.addButton.primaryButton.setTitle(''); + this.addButton.dropdownButton.setTitle(''); + } else { + this.addButtonSimple.label = label; + this.addButtonSimple.enabled = true; + this.addButtonSimple.setTitle(''); + } } } @@ -787,35 +819,16 @@ export class AICustomizationListWidget extends Disposable { }); } } else if (promptType === PromptsType.hook) { - // Parse hook files and display individual hooks - const workspaceFolder = this.workspaceContextService.getWorkspace().folders[0]; - const workspaceRootUri = workspaceFolder?.uri; - const userHomeUri = await this.pathService.userHome(); - const userHome = userHomeUri.fsPath ?? userHomeUri.path; - const remoteEnv = await this.remoteAgentService.getEnvironment(); - const targetOS = remoteEnv?.os ?? OS; - - const parsedHooks = await parseAllHookFiles( - this.promptsService, - this.fileService, - this.labelService, - workspaceRootUri, - userHome, - targetOS, - CancellationToken.None - ); - - for (const hook of parsedHooks) { - // Determine storage from the file path - const storage = hook.filePath.startsWith('~') ? PromptsStorage.user : PromptsStorage.local; - + // Show hook files (not individual hooks) so users can open and edit them + const hookFiles = await this.promptsService.listPromptFiles(PromptsType.hook, CancellationToken.None); + for (const hookFile of hookFiles) { + const filename = basename(hookFile.uri); items.push({ - id: `${hook.fileUri.toString()}#${hook.hookType}-${hook.index}`, - uri: hook.fileUri, - name: `${hook.hookTypeLabel}: ${hook.commandLabel}`, - filename: basename(hook.fileUri), - description: hook.filePath, - storage, + id: hookFile.uri.toString(), + uri: hookFile.uri, + name: this.getFriendlyName(filename), + filename, + storage: hookFile.storage, promptType, }); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 8095e29d8a6c2..3dc9d31a6b432 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -26,7 +26,7 @@ import { IListVirtualDelegate, IListRenderer } from '../../../../../base/browser import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { basename, isEqual } from '../../../../../base/common/resources.js'; +import { basename, isEqual, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { registerColor } from '../../../../../platform/theme/common/colorRegistry.js'; import { PANEL_BORDER } from '../../../../common/theme.js'; @@ -62,6 +62,9 @@ import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditor import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { IPathService } from '../../../../services/path/common/pathService.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { HOOKS_SOURCE_FOLDER } from '../../common/promptSyntax/config/promptFileLocations.js'; import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput.js'; import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js'; import { IWorkbenchMcpServer } from '../../../mcp/common/mcpTypes.js'; @@ -185,6 +188,7 @@ export class AICustomizationManagementEditor extends EditorPane { @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @ITextFileService private readonly textFileService: ITextFileService, @IPathService private readonly pathService: IPathService, + @IFileService private readonly fileService: IFileService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); @@ -503,13 +507,18 @@ export class AICustomizationManagementEditor extends EditorPane { private async createNewItemManual(type: PromptsType, target: 'workspace' | 'user'): Promise { if (type === PromptsType.hook) { - const isWorkspace = target === 'workspace'; - await this.instantiationService.invokeFunction(showConfigureHooksQuickPick, { - openEditor: async (resource) => { - await this.showEmbeddedEditor(resource, basename(resource), isWorkspace); - return; - }, - }); + if (this.workspaceService.preferManualCreation) { + // Sessions: directly create a Copilot CLI format hooks file + await this.createCopilotCliHookFile(); + } else { + // Core: show the configure hooks quick pick + await this.instantiationService.invokeFunction(showConfigureHooksQuickPick, { + openEditor: async (resource) => { + await this.showEmbeddedEditor(resource, basename(resource), true); + return; + }, + }); + } return; } @@ -540,6 +549,46 @@ export class AICustomizationManagementEditor extends EditorPane { void this.listWidget.refresh(); } + /** + * Ensures a Copilot CLI format hooks file exists (.github/hooks/hooks.json), + * then opens the configure hooks quick pick. + */ + private async createCopilotCliHookFile(): Promise { + const projectRoot = this.workspaceService.getActiveProjectRoot(); + if (!projectRoot) { + return; + } + + const hookFileUri = joinPath(projectRoot, HOOKS_SOURCE_FOLDER, 'hooks.json'); + + // Create the file with all hook events if it doesn't exist + try { + await this.fileService.stat(hookFileUri); + } catch { + const hooksContent = { + hooks: { + sessionStart: [ + { type: 'command', command: '' } + ], + userPromptSubmitted: [ + { type: 'command', command: '' } + ], + preToolUse: [ + { type: 'command', command: '' } + ], + postToolUse: [ + { type: 'command', command: '' } + ], + } + }; + const jsonContent = JSON.stringify(hooksContent, null, '\t'); + await this.fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent)); + } + + await this.showEmbeddedEditor(hookFileUri, basename(hookFileUri), true); + void this.listWidget.refresh(); + } + override updateStyles(): void { const borderColor = this.theme.getColor(aiCustomizationManagementSashBorder); if (borderColor) { From ecd686d6162092911e13ebd44dbdd38dc7085765 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:45:17 -0800 Subject: [PATCH 05/10] customizations: fix tooltip hovers over modals (#297883) --- .../aiCustomization/aiCustomizationManagementEditor.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 3dc9d31a6b432..1aa934879ae94 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -57,7 +57,6 @@ import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWo import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js'; import { getSimpleEditorOptions } from '../../../codeEditor/browser/simpleEditorOptions.js'; import { IWorkingCopyService } from '../../../../services/workingCopy/common/workingCopyService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; @@ -184,7 +183,6 @@ export class AICustomizationManagementEditor extends EditorPane { @IPromptsService private readonly promptsService: IPromptsService, @ITextModelService private readonly textModelService: ITextModelService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILayoutService private readonly layoutService: ILayoutService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @ITextFileService private readonly textFileService: ITextFileService, @IPathService private readonly pathService: IPathService, @@ -683,7 +681,7 @@ export class AICustomizationManagementEditor extends EditorPane { this.editorSaveIndicator = DOM.append(editorHeader, $('.editor-save-indicator')); const embeddedEditorContainer = DOM.append(this.editorContentContainer, $('.embedded-editor-container')); - const overflowWidgetsDomNode = this.layoutService.getContainer(DOM.getWindow(embeddedEditorContainer)).appendChild($('.embedded-editor-overflow-widgets.monaco-editor')); + const overflowWidgetsDomNode = DOM.append(this.editorContentContainer, $('.embedded-editor-overflow-widgets.monaco-editor')); this.editorDisposables.add(toDisposable(() => overflowWidgetsDomNode.remove())); this.embeddedEditor = this.editorDisposables.add(this.instantiationService.createInstance( From 85682c5431d0b81314371e82a473c34131c6433b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 08:08:23 +0100 Subject: [PATCH 06/10] modal - fix the default (#297885) --- src/vs/workbench/browser/workbench.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 77efbf97beeac..058693c7cfdfa 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -359,7 +359,7 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('useModal.all', "All editors open in a centered modal overlay."), ], 'description': localize('useModal', "Controls whether editors open in a modal overlay."), - 'default': 'off', // TODO@bpasero figure out the default + 'default': product.quality !== 'stable' ? 'some' : 'off', // TODO@bpasero figure out the default tags: ['experimental'], experiment: { mode: 'auto' From e8927720b929bc2ae7cdc63136806f41a0103ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 26 Feb 2026 08:10:30 +0100 Subject: [PATCH 07/10] fix: set 'u' query parameter to 'none' if internalOrg is not provided (#297889) --- src/vs/platform/update/electron-main/abstractUpdateService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 5470a3553baba..3e5956faa22fd 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -30,9 +30,7 @@ export function createUpdateURL(baseUpdateUrl: string, platform: string, quality url.searchParams.set('bg', 'true'); } - if (options?.internalOrg) { - url.searchParams.set('org', options.internalOrg); - } + url.searchParams.set('u', options?.internalOrg ?? 'none'); return url.toString(); } From d31e0c397add00fb793d16d1644bdad95f60922c Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 26 Feb 2026 09:43:41 +0100 Subject: [PATCH 08/10] Missing user-level claude agents (#297894) --- .../chat/common/promptSyntax/config/promptFileLocations.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts index 7b301db8f2c2e..70de863c8f815 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts @@ -185,6 +185,7 @@ export const DEFAULT_PROMPT_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ export const DEFAULT_AGENT_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ { path: AGENTS_SOURCE_FOLDER, source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local }, { path: CLAUDE_AGENTS_SOURCE_FOLDER, source: PromptFileSource.ClaudeWorkspace, storage: PromptsStorage.local }, + { path: '~/' + CLAUDE_AGENTS_SOURCE_FOLDER, source: PromptFileSource.ClaudePersonal, storage: PromptsStorage.user }, ]; /** From f1b9cacfe7084787842baa35bff96c018bee7ca0 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:44:02 -0800 Subject: [PATCH 09/10] make sure not to strip new lines for hooks (#297896) make sure to break on new lines --- .../widget/chatContentParts/media/chatHookContentPart.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css index 24d64e65bb7a6..b019d98a50a8c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatHookContentPart.css @@ -22,12 +22,14 @@ .chat-hook-reason { font-size: var(--vscode-chat-font-size-body-s); padding: 4px 10px; + white-space: pre-wrap; } .chat-hook-message { font-size: var(--vscode-chat-font-size-body-s); padding: 4px 10px; color: var(--vscode-descriptionForeground); + white-space: pre-wrap; } /* When both reason and message are shown, add a subtle separator */ From c0e2678a6a9e2c385ca58030aabcdb222fc5cce2 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 26 Feb 2026 10:26:43 +0100 Subject: [PATCH 10/10] Naming of instructions files, use constants, make consistent (#297742) * Naming of instruction files, use constants, make consistent * update * update --- .../chat/browser/actions/chatActions.ts | 32 ++++++------- .../aiCustomizationWorkspaceService.ts | 4 +- .../contrib/chat/browser/chatTipService.ts | 47 ++++++++++--------- .../createSlashCommandsUsageTracker.ts | 4 +- .../promptSyntax/newPromptFileActions.ts | 4 +- .../promptSyntax/pickers/promptFilePickers.ts | 30 ++++++------ .../contrib/chat/browser/widget/chatWidget.ts | 7 ++- .../chat/test/browser/chatTipService.test.ts | 23 ++++----- .../areas/accessibility/accessibility.test.ts | 8 ++-- 9 files changed, 81 insertions(+), 78 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index e881fc4c33631..f2cfdd54ec47d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -79,8 +79,8 @@ export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup'; export const CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID = 'workbench.action.chat.triggerSetupSupportAnonymousAction'; const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle'; -export const GENERATE_INSTRUCTIONS_COMMAND_ID = 'workbench.action.chat.generateInstructions'; -export const GENERATE_INSTRUCTION_COMMAND_ID = 'workbench.action.chat.generateInstruction'; +export const GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID = 'workbench.action.chat.generateAgentInstructions'; +export const GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID = 'workbench.action.chat.generateOnDemandInstructions'; export const GENERATE_PROMPT_COMMAND_ID = 'workbench.action.chat.generatePrompt'; export const GENERATE_SKILL_COMMAND_ID = 'workbench.action.chat.generateSkill'; export const GENERATE_AGENT_COMMAND_ID = 'workbench.action.chat.generateAgent'; @@ -1235,9 +1235,8 @@ export function registerChatActions() { registerAction2(class GenerateInstructionsAction extends Action2 { constructor() { super({ - id: GENERATE_INSTRUCTIONS_COMMAND_ID, - title: localize2('generateInstructions', "Generate Workspace Instructions with Agent"), - shortTitle: localize2('generateInstructions.short', "Generate Instructions with Agent"), + id: GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID, + title: localize2('generateInstructions', "Generate Agent Instructions"), category: CHAT_CATEGORY, icon: Codicon.sparkle, f1: true, @@ -1258,9 +1257,8 @@ export function registerChatActions() { registerAction2(class GenerateInstructionAction extends Action2 { constructor() { super({ - id: GENERATE_INSTRUCTION_COMMAND_ID, - title: localize2('generateInstruction', "Generate On-demand Instruction with Agent"), - shortTitle: localize2('generateInstruction.short', "Generate Instruction with Agent"), + id: GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, + title: localize2('generateOnDemandInstructions', "Generate On-Demand Instructions"), category: CHAT_CATEGORY, icon: Codicon.sparkle, f1: true, @@ -1272,7 +1270,7 @@ export function registerChatActions() { const commandService = accessor.get(ICommandService); await commandService.executeCommand('workbench.action.chat.open', { mode: 'agent', - query: '/create-instruction ', + query: '/create-instructions ', isPartialQuery: true, }); } @@ -1282,8 +1280,8 @@ export function registerChatActions() { constructor() { super({ id: GENERATE_PROMPT_COMMAND_ID, - title: localize2('generatePrompt', "Generate Prompt File with Agent"), - shortTitle: localize2('generatePrompt.short', "Generate Prompt with Agent"), + title: localize2('generatePrompt', "Generate Prompt File"), + shortTitle: localize2('generatePrompt.short', "Generate Prompt"), category: CHAT_CATEGORY, icon: Codicon.sparkle, f1: true, @@ -1305,8 +1303,8 @@ export function registerChatActions() { constructor() { super({ id: GENERATE_SKILL_COMMAND_ID, - title: localize2('generateSkill', "Generate Skill with Agent"), - shortTitle: localize2('generateSkill.short', "Generate Skill with Agent"), + title: localize2('generateSkill', "Generate Skill"), + shortTitle: localize2('generateSkill.short', "Generate Skill"), category: CHAT_CATEGORY, icon: Codicon.sparkle, f1: true, @@ -1328,8 +1326,8 @@ export function registerChatActions() { constructor() { super({ id: GENERATE_AGENT_COMMAND_ID, - title: localize2('generateAgent', "Generate Custom Agent with Agent"), - shortTitle: localize2('generateAgent.short', "Generate Agent with Agent"), + title: localize2('generateAgent', "Generate Custom Agent"), + shortTitle: localize2('generateAgent.short', "Generate Agent"), category: CHAT_CATEGORY, icon: Codicon.sparkle, f1: true, @@ -1351,8 +1349,8 @@ export function registerChatActions() { constructor() { super({ id: GENERATE_HOOK_COMMAND_ID, - title: localize2('generateHook', "Generate Hook with Agent"), - shortTitle: localize2('generateHook.short', "Generate Hook with Agent"), + title: localize2('generateHook', "Generate Hook"), + shortTitle: localize2('generateHook.short', "Generate Hook"), category: CHAT_CATEGORY, icon: Codicon.sparkle, f1: true, diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 07684fb6a1084..50a5f6acc17c9 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -14,7 +14,7 @@ import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; import { GENERATE_AGENT_COMMAND_ID, GENERATE_HOOK_COMMAND_ID, - GENERATE_INSTRUCTION_COMMAND_ID, + GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, GENERATE_PROMPT_COMMAND_ID, GENERATE_SKILL_COMMAND_ID, } from '../actions/chatActions.js'; @@ -76,7 +76,7 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic const commandIds: Partial> = { [PromptsType.agent]: GENERATE_AGENT_COMMAND_ID, [PromptsType.skill]: GENERATE_SKILL_COMMAND_ID, - [PromptsType.instructions]: GENERATE_INSTRUCTION_COMMAND_ID, + [PromptsType.instructions]: GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, [PromptsType.prompt]: GENERATE_PROMPT_COMMAND_ID, [PromptsType.hook]: GENERATE_HOOK_COMMAND_ID, }; diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 7f2559f5895ed..c830a665e7a22 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -26,6 +26,7 @@ import { CreateSlashCommandsUsageTracker } from './createSlashCommandsUsageTrack import { ChatEntitlement, IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, IParsedChatRequest } from '../common/requestParser/chatParserTypes.js'; +import { GENERATE_AGENT_COMMAND_ID, GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, GENERATE_PROMPT_COMMAND_ID, GENERATE_SKILL_COMMAND_ID } from './actions/chatActions.js'; type ChatTipEvent = { tipId: string; @@ -41,11 +42,11 @@ type ChatTipClassification = { comment: 'Tracks user interactions with chat tips to understand which tips resonate and which are dismissed.'; }; -const ATTACH_FILES_REFERENCE_TRACKING_COMMAND = 'chat.tips.attachFiles.referenceUsed'; -const CREATE_INSTRUCTION_TRACKING_COMMAND = 'chat.tips.createInstruction.commandUsed'; -const CREATE_PROMPT_TRACKING_COMMAND = 'chat.tips.createPrompt.commandUsed'; -const CREATE_AGENT_TRACKING_COMMAND = 'chat.tips.createAgent.commandUsed'; -const CREATE_SKILL_TRACKING_COMMAND = 'chat.tips.createSkill.commandUsed'; +export const ATTACH_FILES_REFERENCE_TRACKING_COMMAND = 'chat.tips.attachFiles.referenceUsed'; +export const CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND = 'chat.tips.createAgentInstructions.commandUsed'; +export const CREATE_PROMPT_TRACKING_COMMAND = 'chat.tips.createPrompt.commandUsed'; +export const CREATE_AGENT_TRACKING_COMMAND = 'chat.tips.createAgent.commandUsed'; +export const CREATE_SKILL_TRACKING_COMMAND = 'chat.tips.createSkill.commandUsed'; export const IChatTipService = createDecorator('chatTipService'); @@ -197,25 +198,27 @@ const TIP_CATALOG: ITipDefinition[] = [ id: 'tip.createInstruction', message: localize( 'tip.createInstruction', - "Tip: Use [/create-instruction](command:workbench.action.chat.generateInstruction) to generate an on-demand instruction file with the agent." + "Tip: Use [/create-instructions](command:{0}) to generate an on-demand instructions file with the agent.", + GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID ), when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), - enabledCommands: ['workbench.action.chat.generateInstruction'], + enabledCommands: [GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID], excludeWhenCommandsExecuted: [ - 'workbench.action.chat.generateInstruction', - CREATE_INSTRUCTION_TRACKING_COMMAND, + GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, + CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, ], }, { id: 'tip.createPrompt', message: localize( 'tip.createPrompt', - "Tip: Use [/create-prompt](command:workbench.action.chat.generatePrompt) to generate a reusable prompt file with the agent." + "Tip: Use [/create-prompt](command:{0}) to generate a reusable prompt file with the agent.", + GENERATE_PROMPT_COMMAND_ID ), when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), - enabledCommands: ['workbench.action.chat.generatePrompt'], + enabledCommands: [GENERATE_PROMPT_COMMAND_ID], excludeWhenCommandsExecuted: [ - 'workbench.action.chat.generatePrompt', + GENERATE_PROMPT_COMMAND_ID, CREATE_PROMPT_TRACKING_COMMAND, ], }, @@ -223,12 +226,13 @@ const TIP_CATALOG: ITipDefinition[] = [ id: 'tip.createAgent', message: localize( 'tip.createAgent', - "Tip: Use [/create-agent](command:workbench.action.chat.generateAgent) to scaffold a custom agent for your workflow." + "Tip: Use [/create-agent](command:{0}) to scaffold a custom agent for your workflow.", + GENERATE_AGENT_COMMAND_ID ), when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), - enabledCommands: ['workbench.action.chat.generateAgent'], + enabledCommands: [GENERATE_AGENT_COMMAND_ID], excludeWhenCommandsExecuted: [ - 'workbench.action.chat.generateAgent', + GENERATE_AGENT_COMMAND_ID, CREATE_AGENT_TRACKING_COMMAND, ], }, @@ -236,12 +240,13 @@ const TIP_CATALOG: ITipDefinition[] = [ id: 'tip.createSkill', message: localize( 'tip.createSkill', - "Tip: Use [/create-skill](command:workbench.action.chat.generateSkill) to create a skill the agent can load when relevant." + "Tip: Use [/create-skill](command:{0}) to create a skill the agent can load when relevant.", + GENERATE_SKILL_COMMAND_ID ), when: ChatContextKeys.chatSessionType.isEqualTo(localChatSessionType), - enabledCommands: ['workbench.action.chat.generateSkill'], + enabledCommands: [GENERATE_SKILL_COMMAND_ID], excludeWhenCommandsExecuted: [ - 'workbench.action.chat.generateSkill', + GENERATE_SKILL_COMMAND_ID, CREATE_SKILL_TRACKING_COMMAND, ], }, @@ -759,14 +764,14 @@ export class ChatTipService extends Disposable implements IChatTipService { } const trimmed = message.text.trimStart(); - const match = /^\/(create-(?:instruction|prompt|agent|skill))(?:\s|$)/.exec(trimmed); + const match = /^\/(create-(?:instructions|prompt|agent|skill))(?:\s|$)/.exec(trimmed); return match ? this._toCreateSlashCommandTrackingId(match[1]) : undefined; } private _toCreateSlashCommandTrackingId(command: string): string | undefined { switch (command) { - case 'create-instruction': - return CREATE_INSTRUCTION_TRACKING_COMMAND; + case 'create-instructions': + return CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND; case 'create-prompt': return CREATE_PROMPT_TRACKING_COMMAND; case 'create-agent': diff --git a/src/vs/workbench/contrib/chat/browser/createSlashCommandsUsageTracker.ts b/src/vs/workbench/contrib/chat/browser/createSlashCommandsUsageTracker.ts index c22041aa98018..eea1a645a61ed 100644 --- a/src/vs/workbench/contrib/chat/browser/createSlashCommandsUsageTracker.ts +++ b/src/vs/workbench/contrib/chat/browser/createSlashCommandsUsageTracker.ts @@ -38,7 +38,7 @@ export class CreateSlashCommandsUsageTracker extends Disposable { // Fallback when parsing doesn't produce a slash command part. const trimmed = message.text.trimStart(); - const match = /^\/(create-(?:instruction|prompt|agent|skill))(?:\s|$)/.exec(trimmed); + const match = /^\/(create-(?:instructions|prompt|agent|skill))(?:\s|$)/.exec(trimmed); if (match && CreateSlashCommandsUsageTracker._isCreateSlashCommand(match[1])) { this._markUsed(); } @@ -65,7 +65,7 @@ export class CreateSlashCommandsUsageTracker extends Disposable { private static _isCreateSlashCommand(command: string): boolean { switch (command) { - case 'create-instruction': + case 'create-instructions': case 'create-prompt': case 'create-agent': case 'create-skill': diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts index 15f6f5b34f604..fc1d34ba0996f 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/newPromptFileActions.ts @@ -186,7 +186,7 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `. - "src/**/*.ts"`, `---`, ``, - ``, + ``, ``, `\${2:Provide coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`, ].join('\n'); @@ -197,7 +197,7 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi `# applyTo: '\${1|**,**/*.ts|}' # when provided, instructions will automatically be added to the request context when the pattern matches an attached file`, `---`, ``, - ``, + ``, ``, `\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`, ].join('\n'); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts index 3f932d05506c2..54b363863fbcd 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts @@ -17,7 +17,7 @@ import { ICommandService } from '../../../../../../platform/commands/common/comm import { getCleanPromptName } from '../../../common/promptSyntax/config/promptFileLocations.js'; import { PromptsType, INSTRUCTIONS_DOCUMENTATION_URL, AGENT_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL } from '../../../common/promptSyntax/promptTypes.js'; import { NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../newPromptFileActions.js'; -import { GENERATE_INSTRUCTIONS_COMMAND_ID, GENERATE_INSTRUCTION_COMMAND_ID, GENERATE_PROMPT_COMMAND_ID, GENERATE_SKILL_COMMAND_ID, GENERATE_AGENT_COMMAND_ID } from '../../actions/chatActions.js'; +import { GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID, GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, GENERATE_PROMPT_COMMAND_ID, GENERATE_SKILL_COMMAND_ID, GENERATE_AGENT_COMMAND_ID } from '../../actions/chatActions.js'; import { IKeyMods, IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from '../../../../../../platform/quickinput/common/quickInput.js'; import { askForPromptFileName } from './askForPromptName.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -170,33 +170,33 @@ const NEW_INSTRUCTIONS_FILE_OPTION: IPromptPickerQuickPickItem = { }; /** - * A quick pick item that starts the 'Generate Workspace Instructions' command. + * A quick pick item that starts the 'Generate Agent Instructions' command. */ -const GENERATE_WORKSPACE_INSTRUCTIONS_OPTION: IPromptPickerQuickPickItem = { +const GENERATE_AGENT_INSTRUCTIONS_OPTION: IPromptPickerQuickPickItem = { type: 'item', label: `$(sparkle) ${localize( - 'commands.generate-workspace-instructions.select-dialog.label', - 'Generate workspace instructions with agent...', + 'commands.generate-agent-instructions.select-dialog.label', + 'Generate agent instructions...', )}`, pickable: false, alwaysShow: true, buttons: [newHelpButton(PromptsType.instructions)], - commandId: GENERATE_INSTRUCTIONS_COMMAND_ID, + commandId: GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID, }; /** - * A quick pick item that starts the 'Generate On-demand Instruction' command. + * A quick pick item that starts the 'Generate On-demand Instructions' command. */ -const GENERATE_INSTRUCTION_OPTION: IPromptPickerQuickPickItem = { +const GENERATE_ON_DEMAND_INSTRUCTIONS_OPTION: IPromptPickerQuickPickItem = { type: 'item', label: `$(sparkle) ${localize( - 'commands.generate-instruction.select-dialog.label', - 'Generate on-demand instruction with agent...', + 'commands.generate-on-demand-instructions.select-dialog.label', + 'Generate on-demand instructions...', )}`, pickable: false, alwaysShow: true, buttons: [newHelpButton(PromptsType.instructions)], - commandId: GENERATE_INSTRUCTION_COMMAND_ID, + commandId: GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, }; /** @@ -236,7 +236,7 @@ const GENERATE_PROMPT_OPTION: IPromptPickerQuickPickItem = { type: 'item', label: `$(sparkle) ${localize( 'commands.generate-prompt.select-dialog.label', - 'Generate prompt with agent...', + 'Generate prompt...', )}`, pickable: false, alwaysShow: true, @@ -251,7 +251,7 @@ const GENERATE_SKILL_OPTION: IPromptPickerQuickPickItem = { type: 'item', label: `$(sparkle) ${localize( 'commands.generate-skill.select-dialog.label', - 'Generate skill with agent...', + 'Generate skill...', )}`, pickable: false, alwaysShow: true, @@ -266,7 +266,7 @@ const GENERATE_AGENT_OPTION: IPromptPickerQuickPickItem = { type: 'item', label: `$(sparkle) ${localize( 'commands.generate-agent.select-dialog.label', - 'Generate agent with agent...', + 'Generate agent...', )}`, pickable: false, alwaysShow: true, @@ -537,7 +537,7 @@ export class PromptFilePickers { case PromptsType.prompt: return [NEW_PROMPT_FILE_OPTION, GENERATE_PROMPT_OPTION]; case PromptsType.instructions: - return [NEW_INSTRUCTIONS_FILE_OPTION, GENERATE_INSTRUCTION_OPTION, GENERATE_WORKSPACE_INSTRUCTIONS_OPTION]; + return [NEW_INSTRUCTIONS_FILE_OPTION, GENERATE_ON_DEMAND_INSTRUCTIONS_OPTION, GENERATE_AGENT_INSTRUCTIONS_OPTION]; case PromptsType.agent: return [NEW_AGENT_FILE_OPTION, GENERATE_AGENT_OPTION]; case PromptsType.skill: diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 02c2121487809..48e7be78cd212 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -72,7 +72,7 @@ import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeA import { PromptsConfig } from '../../common/promptSyntax/config/config.js'; import { IHandOff, PromptHeader } from '../../common/promptSyntax/promptFileParser.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; -import { handleModeSwitch } from '../actions/chatActions.js'; +import { GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID, handleModeSwitch } from '../actions/chatActions.js'; import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from '../chat.js'; import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js'; import { IChatAttachmentResolveService } from '../attachments/chatAttachmentResolveService.js'; @@ -1077,12 +1077,11 @@ export class ChatWidget extends Disposable implements IChatWidget { return new MarkdownString(''); } else if (this._instructionFilesExist === false) { // Show generate instructions message if no files exist - const generateInstructionsCommand = 'workbench.action.chat.generateInstructions'; return new MarkdownString(localize( 'chatWidget.instructions', "[Generate Agent Instructions]({0}) to onboard AI onto your codebase.", - `command:${generateInstructionsCommand}` - ), { isTrusted: { enabledCommands: [generateInstructionsCommand] } }); + `command:${GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID}` + ), { isTrusted: { enabledCommands: [GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID] } }); } // While checking, don't show the generate instructions message diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index 8be5fe9be000b..814c6c740b9ab 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -15,7 +15,7 @@ import { MockContextKeyService } from '../../../../../platform/keybinding/test/c import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { ChatTipService, IChatTip, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; +import { ChatTipService, CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, CREATE_AGENT_TRACKING_COMMAND, CREATE_PROMPT_TRACKING_COMMAND, CREATE_SKILL_TRACKING_COMMAND, IChatTip, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; import { AgentFileType, IPromptPath, IPromptsService, IResolvedAgentFile, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -34,6 +34,7 @@ import { Range } from '../../../../../editor/common/core/range.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; import { localChatSessionType } from '../../common/chatSessionsService.js'; +import { GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID } from '../../browser/actions/chatActions.js'; class MockContextKeyServiceWithRulesMatching extends MockContextKeyService { override contextMatchesRules(rules: ContextKeyExpression): boolean { @@ -167,10 +168,10 @@ suite('ChatTipService', () => { }); const executedCommands = JSON.parse(storageService.get('chat.tips.executedCommands', StorageScope.APPLICATION) ?? '[]') as string[]; - assert.ok(executedCommands.includes('chat.tips.createPrompt.commandUsed')); - assert.ok(!executedCommands.includes('chat.tips.createInstruction.commandUsed')); - assert.ok(!executedCommands.includes('chat.tips.createAgent.commandUsed')); - assert.ok(!executedCommands.includes('chat.tips.createSkill.commandUsed')); + assert.ok(executedCommands.includes(CREATE_PROMPT_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_AGENT_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_SKILL_TRACKING_COMMAND)); }); test('returns Auto switch tip when current model is gpt-4.1', () => { @@ -606,7 +607,7 @@ suite('ChatTipService', () => { const tip: ITipDefinition = { id: 'tip.customInstructions', message: 'test', - excludeWhenCommandsExecuted: ['workbench.action.chat.generateInstructions'], + excludeWhenCommandsExecuted: [GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID], }; const tracker = testDisposables.add(new TipEligibilityTracker( @@ -620,7 +621,7 @@ suite('ChatTipService', () => { assert.strictEqual(tracker.isExcluded(tip), false, 'Should not be excluded before command is executed'); - commandExecutedEmitter.fire({ commandId: 'workbench.action.chat.generateInstructions', args: [] }); + commandExecutedEmitter.fire({ commandId: GENERATE_AGENT_INSTRUCTIONS_COMMAND_ID, args: [] }); assert.strictEqual(tracker.isExcluded(tip), true, 'Should be excluded after generate instructions command is executed'); }); @@ -941,7 +942,7 @@ suite('ChatTipService', () => { }); test('does not show create prompt tip when create prompt was already used', () => { - storageService.store('chat.tips.executedCommands', JSON.stringify(['chat.tips.createPrompt.commandUsed']), StorageScope.APPLICATION, StorageTarget.MACHINE); + storageService.store('chat.tips.executedCommands', JSON.stringify([CREATE_PROMPT_TRACKING_COMMAND]), StorageScope.APPLICATION, StorageTarget.MACHINE); const service = createService(); contextKeyService.createKey(ChatContextKeys.chatSessionType.key, localChatSessionType); @@ -1376,7 +1377,7 @@ suite('CreateSlashCommandsUsageTracker', () => { assert.strictEqual(value, true, 'Context key should be true when create commands have been used'); }); - test('detects create-instruction slash command via text fallback', () => { + test('detects create-instructions slash command via text fallback', () => { const sessionResource = URI.parse('chat:session1'); const tracker = createTracker(); tracker.syncContextKey(contextKeyService); @@ -1384,7 +1385,7 @@ suite('CreateSlashCommandsUsageTracker', () => { sessions.set(sessionResource.toString(), { lastRequest: { message: { - text: '/create-instruction test', + text: '/create-instructions test', parts: [], }, }, @@ -1393,7 +1394,7 @@ suite('CreateSlashCommandsUsageTracker', () => { submitRequestEmitter.fire({ chatSessionResource: sessionResource }); const value = contextKeyService.getContextKeyValue(ChatContextKeys.hasUsedCreateSlashCommands.key); - assert.strictEqual(value, true, 'Context key should be true after /create-instruction is used'); + assert.strictEqual(value, true, 'Context key should be true after /create-instructions is used'); assert.strictEqual( storageService.getBoolean('chat.tips.usedCreateSlashCommands', StorageScope.APPLICATION, false), true, diff --git a/test/smoke/src/areas/accessibility/accessibility.test.ts b/test/smoke/src/areas/accessibility/accessibility.test.ts index ac3868eb55b74..ad16a299c0128 100644 --- a/test/smoke/src/areas/accessibility/accessibility.test.ts +++ b/test/smoke/src/areas/accessibility/accessibility.test.ts @@ -34,7 +34,7 @@ export function setup(logger: Logger, opts: { web?: boolean }, quality: Quality) selector: '.monaco-workbench', excludeRules: { // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect - 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'], + 'link-in-text-block': ['command:workbench.action.chat.generateAgentInstructions'], // Monaco lists use aria-multiselectable on role="list" and aria-setsize/aria-posinset/aria-selected on role="dialog" rows // These violations appear intermittently when notification lists or other dynamic lists are visible // Note: patterns match against HTML string, not CSS selectors, so no leading dots @@ -85,7 +85,7 @@ export function setup(logger: Logger, opts: { web?: boolean }, quality: Quality) selector: 'div[id="workbench.panel.chat"]', excludeRules: { // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect - 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'] + 'link-in-text-block': ['command:workbench.action.chat.generateAgentInstructions'] } }); }); @@ -117,7 +117,7 @@ export function setup(logger: Logger, opts: { web?: boolean }, quality: Quality) selector: 'div[id="workbench.panel.chat"]', excludeRules: { // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect - 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'], + 'link-in-text-block': ['command:workbench.action.chat.generateAgentInstructions'], // Monaco lists use aria-multiselectable on role="list" and aria-selected on role="listitem" // These are used intentionally for selection semantics even though technically not spec-compliant 'aria-allowed-attr': ['monaco-list', 'monaco-list-row'], @@ -157,7 +157,7 @@ export function setup(logger: Logger, opts: { web?: boolean }, quality: Quality) selector: 'div[id="workbench.panel.chat"]', excludeRules: { // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect - 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'], + 'link-in-text-block': ['command:workbench.action.chat.generateAgentInstructions'], // Monaco lists use aria-multiselectable on role="list" and aria-selected on role="listitem" // These are used intentionally for selection semantics even though technically not spec-compliant 'aria-allowed-attr': ['monaco-list', 'monaco-list-row'],