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(); } 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/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' diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 4903908dd4997..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'; @@ -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 }], }); @@ -1187,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, @@ -1210,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, @@ -1224,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, }); } @@ -1234,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, @@ -1257,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, @@ -1280,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, @@ -1303,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/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 5f52802bda9cb..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, }); } @@ -964,7 +977,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/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 8095e29d8a6c2..1aa934879ae94 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'; @@ -57,11 +57,13 @@ 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'; 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'; @@ -181,10 +183,10 @@ 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, + @IFileService private readonly fileService: IFileService, ) { super(AICustomizationManagementEditor.ID, group, telemetryService, themeService, storageService); @@ -503,13 +505,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 +547,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) { @@ -634,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( diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 4f4828681261b..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'; @@ -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[] = []; @@ -72,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/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d8b6675e6cf0c..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'; @@ -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'; @@ -52,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'; @@ -97,6 +99,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'; @@ -169,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); @@ -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(); + } + }); + } } @@ -1544,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'; @@ -1614,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/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/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 */ 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/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 { 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. 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 }, ]; /** 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 } } }, 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'],