Update configure hooks flow and supported paths (#293643)

* update

* updates

* Update src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts

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

* update handling

* PR

* test

* fix test

* cleanup

* nit

* cleanup

* clean

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Paul
2026-02-08 21:02:17 -08:00
committed by GitHub
parent cae3c5a818
commit 55477c4cc8
13 changed files with 526 additions and 357 deletions

View File

@@ -939,7 +939,7 @@ configurationRegistry.registerConfiguration({
title: nls.localize('chat.hookFilesLocations.title', "Hook File Locations",),
markdownDescription: nls.localize(
'chat.hookFilesLocations.description',
"Specify paths to hook configuration files that define custom shell commands to execute at strategic points in an agent's workflow. [Learn More]({0}).\n\nRelative paths are resolved from the root folder(s) of your workspace. Supports Copilot hooks (`hooks.json`) and Claude Code hooks (`settings.json`, `settings.local.json`).",
"Specify paths to hook configuration files that define custom shell commands to execute at strategic points in an agent's workflow. [Learn More]({0}).\n\nRelative paths are resolved from the root folder(s) of your workspace. Supports Copilot hooks (`*.json`) and Claude Code hooks (`settings.json`, `settings.local.json`).",
HOOK_DOCUMENTATION_URL,
),
default: {

View File

@@ -3,6 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isEqual } from '../../../../../base/common/resources.js';
import { URI } from '../../../../../base/common/uri.js';
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { ChatViewId } from '../chat.js';
import { CHAT_CATEGORY, CHAT_CONFIG_MENU_ID } from '../actions/chatActions.js';
import { localize, localize2 } from '../../../../../nls.js';
@@ -11,19 +14,23 @@ import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { HOOK_TYPES, HookType, getEffectiveCommandFieldKey } from '../../common/promptSyntax/hookSchema.js';
import { NEW_HOOK_COMMAND_ID } from './newPromptFileActions.js';
import { getCopilotCliHookTypeName, resolveCopilotCliHookType } from '../../common/promptSyntax/hookCopilotCliCompat.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { ITextEditorSelection } from '../../../../../platform/editor/common/editor.js';
import { findHookCommandSelection, parseAllHookFiles, IParsedHook } from './hookUtils.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { IPathService } from '../../../../services/path/common/pathService.js';
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
import { OS } from '../../../../../base/common/platform.js';
@@ -32,9 +39,198 @@ import { OS } from '../../../../../base/common/platform.js';
*/
const CONFIGURE_HOOKS_ACTION_ID = 'workbench.action.chat.configure.hooks';
interface IHookTypeQuickPickItem extends IQuickPickItem {
readonly hookType: typeof HOOK_TYPES[number];
}
interface IHookQuickPickItem extends IQuickPickItem {
readonly hookEntry?: IParsedHook;
readonly commandId?: string;
readonly isAddNewHook?: boolean;
}
interface IHookFileQuickPickItem extends IQuickPickItem {
readonly fileUri?: URI;
readonly isCreateNewFile?: boolean;
}
/**
* Detects if existing hooks use Copilot CLI naming convention (camelCase).
* Returns true if any existing key matches the Copilot CLI format.
*/
function usesCopilotCliNaming(hooksObj: Record<string, unknown>): boolean {
for (const key of Object.keys(hooksObj)) {
// Check if any key resolves to a Copilot CLI hook type
if (resolveCopilotCliHookType(key) !== undefined) {
return true;
}
}
return false;
}
/**
* Gets the appropriate key name for a hook type based on the naming convention used in the file.
*/
function getHookTypeKeyName(hookTypeId: HookType, useCopilotCliNamingConvention: boolean): string {
if (useCopilotCliNamingConvention) {
const copilotCliName = getCopilotCliHookTypeName(hookTypeId);
if (copilotCliName) {
return copilotCliName;
}
}
// Fall back to PascalCase (enum value)
return hookTypeId;
}
/**
* Adds a hook to an existing hook file.
*/
async function addHookToFile(
hookFileUri: URI,
hookTypeId: HookType,
fileService: IFileService,
editorService: IEditorService,
notificationService: INotificationService,
bulkEditService: IBulkEditService
): Promise<void> {
// Parse existing file
let hooksContent: { hooks: Record<string, unknown[]> };
const fileExists = await fileService.exists(hookFileUri);
if (fileExists) {
const existingContent = await fileService.readFile(hookFileUri);
try {
hooksContent = JSON.parse(existingContent.value.toString());
// Ensure hooks object exists
if (!hooksContent.hooks) {
hooksContent.hooks = {};
}
} catch {
// If parsing fails, show error and open file for user to fix
notificationService.error(localize('commands.new.hook.parseError', "Failed to parse existing hooks file. Please fix the JSON syntax errors and try again."));
await editorService.openEditor({ resource: hookFileUri });
return;
}
} else {
// Create new structure
hooksContent = { hooks: {} };
}
// Detect naming convention from existing keys
const useCopilotCliNamingConvention = usesCopilotCliNaming(hooksContent.hooks);
const hookTypeKeyName = getHookTypeKeyName(hookTypeId, useCopilotCliNamingConvention);
// Also check if there's an existing key for this hook type (with either naming)
// Find existing key that resolves to the same hook type
let existingKeyForType: string | undefined;
for (const key of Object.keys(hooksContent.hooks)) {
const resolvedType = resolveCopilotCliHookType(key);
if (resolvedType === hookTypeId || key === hookTypeId) {
existingKeyForType = key;
break;
}
}
// Use existing key if found, otherwise use the detected naming convention
const keyToUse = existingKeyForType ?? hookTypeKeyName;
// Add the new hook entry (append if hook type already exists)
const newHookEntry = {
type: 'command',
command: ''
};
let newHookIndex: number;
if (!hooksContent.hooks[keyToUse]) {
hooksContent.hooks[keyToUse] = [newHookEntry];
newHookIndex = 0;
} else {
hooksContent.hooks[keyToUse].push(newHookEntry);
newHookIndex = hooksContent.hooks[keyToUse].length - 1;
}
// Write the file
const jsonContent = JSON.stringify(hooksContent, null, '\t');
// Check if the file is already open in an editor
const existingEditor = editorService.editors.find(e => isEqual(e.resource, hookFileUri));
if (existingEditor) {
// File is already open - first focus the editor, then update its model directly
await editorService.openEditor({
resource: hookFileUri,
options: {
pinned: false
}
});
// Get the code editor and update its content directly
const editor = getCodeEditor(editorService.activeTextEditorControl);
if (editor && editor.hasModel() && isEqual(editor.getModel().uri, hookFileUri)) {
const model = editor.getModel();
// Apply the full content replacement using executeEdits
model.pushEditOperations([], [{
range: model.getFullModelRange(),
text: jsonContent
}], () => null);
// Find and apply the selection
const selection = findHookCommandSelection(jsonContent, keyToUse, newHookIndex, 'command');
if (selection && selection.endLineNumber !== undefined && selection.endColumn !== undefined) {
editor.setSelection({
startLineNumber: selection.startLineNumber,
startColumn: selection.startColumn,
endLineNumber: selection.endLineNumber,
endColumn: selection.endColumn
});
editor.revealLineInCenter(selection.startLineNumber);
}
} else {
// Fallback: active editor/model check failed, apply via bulk edit service
await bulkEditService.apply([
new ResourceTextEdit(hookFileUri, { range: new Range(1, 1, Number.MAX_SAFE_INTEGER, 1), text: jsonContent })
], { label: localize('addHook', "Add Hook") });
// Find the selection for the new hook's command field
const selection = findHookCommandSelection(jsonContent, keyToUse, newHookIndex, 'command');
// Re-open editor with selection
await editorService.openEditor({
resource: hookFileUri,
options: {
selection,
pinned: false
}
});
}
} else {
// File is not currently open in an editor
if (!fileExists) {
// File doesn't exist - write new file directly and open
await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent));
} else {
// File exists but isn't open - open it first, then use bulk edit for undo support
await editorService.openEditor({
resource: hookFileUri,
options: { pinned: false }
});
// Apply the edit via bulk edit service for proper undo support
await bulkEditService.apply([
new ResourceTextEdit(hookFileUri, { range: new Range(1, 1, Number.MAX_SAFE_INTEGER, 1), text: jsonContent })
], { label: localize('addHook', "Add Hook") });
}
// Find the selection for the new hook's command field
const selection = findHookCommandSelection(jsonContent, keyToUse, newHookIndex, 'command');
// Open editor with selection (or re-focus if already open)
await editorService.openEditor({
resource: hookFileUri,
options: {
selection,
pinned: false
}
});
}
}
/**
@@ -48,10 +244,11 @@ export async function showConfigureHooksQuickPick(
const quickInputService = accessor.get(IQuickInputService);
const fileService = accessor.get(IFileService);
const labelService = accessor.get(ILabelService);
const commandService = accessor.get(ICommandService);
const editorService = accessor.get(IEditorService);
const workspaceService = accessor.get(IWorkspaceContextService);
const pathService = accessor.get(IPathService);
const notificationService = accessor.get(INotificationService);
const bulkEditService = accessor.get(IBulkEditService);
const remoteAgentService = accessor.get(IRemoteAgentService);
// Get the remote OS (or fall back to local OS)
@@ -63,6 +260,8 @@ export async function showConfigureHooksQuickPick(
const workspaceRootUri = workspaceFolder?.uri;
const userHomeUri = await pathService.userHome();
const userHome = userHomeUri.fsPath ?? userHomeUri.path;
// Parse all hook files upfront to count hooks per type
const hookEntries = await parseAllHookFiles(
promptsService,
fileService,
@@ -73,46 +272,55 @@ export async function showConfigureHooksQuickPick(
CancellationToken.None
);
// Build quick pick items grouped by hook type
const items: (IHookQuickPickItem | IQuickPickSeparator)[] = [];
// Count hooks per type
const hookCountByType = new Map<HookType, number>();
for (const entry of hookEntries) {
hookCountByType.set(entry.hookType, (hookCountByType.get(entry.hookType) ?? 0) + 1);
}
// Add "New Hook..." option at the top
items.push({
label: `$(plus) ${localize('commands.new-hook.label', 'Add new hook...')}`,
commandId: NEW_HOOK_COMMAND_ID,
// Step 1: Show all lifecycle events with hook counts
const hookTypeItems: IHookTypeQuickPickItem[] = HOOK_TYPES.map(hookType => {
const count = hookCountByType.get(hookType.id) ?? 0;
const countLabel = count > 0 ? ` (${count})` : '';
return {
label: `${hookType.label}${countLabel}`,
description: hookType.description,
hookType
};
});
const selectedHookType = await quickInputService.pick(hookTypeItems, {
placeHolder: localize('commands.hooks.selectEvent.placeholder', 'Select a lifecycle event'),
title: localize('commands.hooks.title', 'Hooks')
});
if (!selectedHookType) {
return;
}
// Filter hooks by the selected type
const hooksOfType = hookEntries.filter(h => h.hookType === selectedHookType.hookType.id);
// Step 2: Show "Add new hook" + existing hooks of this type
const hookItems: (IHookQuickPickItem | IQuickPickSeparator)[] = [];
// Add "Add new hook" option at the top
hookItems.push({
label: `$(plus) ${localize('commands.addNewHook.label', 'Add new hook...')}`,
isAddNewHook: true,
alwaysShow: true
});
// Group entries by hook type
const groupedByType = new Map<HookType, IParsedHook[]>();
for (const entry of hookEntries) {
const existing = groupedByType.get(entry.hookType) ?? [];
existing.push(entry);
groupedByType.set(entry.hookType, existing);
}
// Sort hook types by their position in HOOK_TYPES
const sortedHookTypes = Array.from(groupedByType.keys()).sort((a, b) => {
const indexA = HOOK_TYPES.findIndex(h => h.id === a);
const indexB = HOOK_TYPES.findIndex(h => h.id === b);
return indexA - indexB;
});
// Add entries grouped by hook type
for (const hookTypeId of sortedHookTypes) {
const entries = groupedByType.get(hookTypeId)!;
const hookType = HOOK_TYPES.find(h => h.id === hookTypeId)!;
items.push({
// Add existing hooks
if (hooksOfType.length > 0) {
hookItems.push({
type: 'separator',
label: hookType.label
label: localize('existingHooks', "Existing Hooks")
});
for (const entry of entries) {
// Use relative path from labelService for consistent display
for (const entry of hooksOfType) {
const description = labelService.getUriLabel(entry.fileUri, { relative: true });
items.push({
hookItems.push({
label: entry.commandLabel,
description,
hookEntry: entry
@@ -120,51 +328,208 @@ export async function showConfigureHooksQuickPick(
}
}
// Show empty state message if no hooks found
if (hookEntries.length === 0) {
items.push({
type: 'separator',
label: localize('noHooks', "No hooks configured")
// Auto-execute if only "Add new hook" is available (no existing hooks)
let selectedHook: IHookQuickPickItem | undefined;
if (hooksOfType.length === 0) {
selectedHook = hookItems[0] as IHookQuickPickItem;
} else {
selectedHook = await quickInputService.pick(hookItems, {
placeHolder: localize('commands.hooks.selectHook.placeholder', 'Select a hook to open or add a new one'),
title: selectedHookType.hookType.label
});
}
const selected = await quickInputService.pick(items, {
placeHolder: localize('commands.hooks.placeholder', 'Select a hook to open or add a new hook'),
title: localize('commands.hooks.title', 'Hooks')
});
if (!selectedHook) {
return;
}
if (selected) {
if (selected.commandId) {
await commandService.executeCommand(selected.commandId);
} else if (selected.hookEntry) {
const entry = selected.hookEntry;
let selection: ITextEditorSelection | undefined;
// Handle clicking on existing hook (focus into command)
if (selectedHook.hookEntry) {
const entry = selectedHook.hookEntry;
let selection: ITextEditorSelection | undefined;
// Determine the command field name to highlight based on target platform
const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS);
// Determine the command field name to highlight based on target platform
const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS);
// Try to find the command field to highlight
if (commandFieldName) {
try {
const content = await fileService.readFile(entry.fileUri);
selection = findHookCommandSelection(
content.value.toString(),
entry.originalHookTypeId,
entry.index,
commandFieldName
);
} catch {
// Ignore errors and just open without selection
}
// Try to find the command field to highlight
if (commandFieldName) {
try {
const content = await fileService.readFile(entry.fileUri);
selection = findHookCommandSelection(
content.value.toString(),
entry.originalHookTypeId,
entry.index,
commandFieldName
);
} catch {
// Ignore errors and just open without selection
}
}
await editorService.openEditor({
resource: entry.fileUri,
options: {
selection,
pinned: false
}
});
return;
}
// Step 3: Handle "Add new hook" - show create new file + existing hook files
if (selectedHook.isAddNewHook) {
// Get existing hook files (local storage only, not User Data)
const hookFiles = await promptsService.listPromptFilesForStorage(PromptsType.hook, PromptsStorage.local, CancellationToken.None);
const fileItems: (IHookFileQuickPickItem | IQuickPickSeparator)[] = [];
// Add "Create new hook config file" option at the top
fileItems.push({
label: `$(new-file) ${localize('commands.createNewHookFile.label', 'Create new hook config file...')}`,
isCreateNewFile: true,
alwaysShow: true
});
// Add existing hook files
if (hookFiles.length > 0) {
fileItems.push({
type: 'separator',
label: localize('existingHookFiles', "Existing Hook Files")
});
for (const hookFile of hookFiles) {
const relativePath = labelService.getUriLabel(hookFile.uri, { relative: true });
fileItems.push({
label: relativePath,
fileUri: hookFile.uri
});
}
}
// Auto-execute if no existing hook files
let selectedFile: IHookFileQuickPickItem | undefined;
if (hookFiles.length === 0) {
selectedFile = fileItems[0] as IHookFileQuickPickItem;
} else {
selectedFile = await quickInputService.pick(fileItems, {
placeHolder: localize('commands.hooks.selectFile.placeholder', 'Select a hook file or create a new one'),
title: localize('commands.hooks.addHook.title', 'Add Hook')
});
}
if (!selectedFile) {
return;
}
// Handle creating new hook config file
if (selectedFile.isCreateNewFile) {
// Get source folders for hooks, filter to local storage only (no User Data)
const allFolders = await promptsService.getSourceFolders(PromptsType.hook);
const localFolders = allFolders.filter(f => f.storage === PromptsStorage.local);
if (localFolders.length === 0) {
notificationService.error(localize('commands.hook.noLocalFolders', "No local hook folder found. Please configure a hooks folder in your workspace."));
return;
}
// Auto-select if only one folder, otherwise show picker
let selectedFolder = localFolders[0];
if (localFolders.length > 1) {
const folderItems = localFolders.map(folder => ({
label: labelService.getUriLabel(folder.uri, { relative: true }),
folder
}));
const pickedFolder = await quickInputService.pick(folderItems, {
placeHolder: localize('commands.hook.selectFolder.placeholder', 'Select a location for the hook file'),
title: localize('commands.hook.selectFolder.title', 'Hook File Location')
});
if (!pickedFolder) {
return;
}
selectedFolder = pickedFolder.folder;
}
// Ask for filename
const fileName = await quickInputService.input({
prompt: localize('commands.hook.filename.prompt', "Enter hook file name"),
placeHolder: localize('commands.hook.filename.placeholder', "e.g., hooks, diagnostics, security"),
validateInput: async (value) => {
if (!value || !value.trim()) {
return localize('commands.hook.filename.required', "File name is required");
}
const name = value.trim();
// Basic validation - no path separators or invalid characters
if (/[/\\:*?"<>|]/.test(name)) {
return localize('commands.hook.filename.invalidChars', "File name contains invalid characters");
}
return undefined;
}
});
if (!fileName) {
return;
}
// Create the hooks folder if it doesn't exist
await fileService.createFolder(selectedFolder.uri);
// Use user-provided filename with .json extension
const hookFileName = fileName.trim().endsWith('.json') ? fileName.trim() : `${fileName.trim()}.json`;
const hookFileUri = URI.joinPath(selectedFolder.uri, hookFileName);
// Check if file already exists
if (await fileService.exists(hookFileUri)) {
// File exists - add hook to it instead of creating new
await addHookToFile(
hookFileUri,
selectedHookType.hookType.id as HookType,
fileService,
editorService,
notificationService,
bulkEditService
);
return;
}
// Create new hook file with the selected hook type
const hooksContent = {
hooks: {
[selectedHookType.hookType.id]: [
{
type: 'command',
command: ''
}
]
}
};
const jsonContent = JSON.stringify(hooksContent, null, '\t');
await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent));
// Find the selection for the new hook's command field
const selection = findHookCommandSelection(jsonContent, selectedHookType.hookType.id, 0, 'command');
// Open editor with selection
await editorService.openEditor({
resource: entry.fileUri,
resource: hookFileUri,
options: {
selection,
pinned: false
}
});
return;
}
// Handle adding hook to existing file
if (selectedFile.fileUri) {
await addHookToFile(
selectedFile.fileUri,
selectedHookType.hookType.id as HookType,
fileService,
editorService,
notificationService,
bulkEditService
);
}
}
}

View File

@@ -5,7 +5,6 @@
import { isEqual } from '../../../../../base/common/resources.js';
import { URI } from '../../../../../base/common/uri.js';
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { SnippetController2 } from '../../../../../editor/contrib/snippet/browser/snippetController2.js';
import { localize, localize2 } from '../../../../../nls.js';
@@ -26,11 +25,7 @@ import { CHAT_CATEGORY } from '../actions/chatActions.js';
import { askForPromptFileName } from './pickers/askForPromptName.js';
import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
import { getCleanPromptName, SKILL_FILENAME, HOOKS_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';
import { HOOK_TYPES, HookType } from '../../common/promptSyntax/hookSchema.js';
import { findHookCommandSelection } from './hookUtils.js';
import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { getCleanPromptName, SKILL_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';
class AbstractNewPromptFileAction extends Action2 {
@@ -180,11 +175,6 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi
`---`,
`\${3:Define the functionality provided by this skill, including detailed instructions and examples}`,
].join('\n');
case PromptsType.hook:
return JSON.stringify({
version: 1,
hooks: {}
}, null, 4);
default:
throw new Error(`Unsupported prompt type: ${promptType}`);
}
@@ -196,7 +186,6 @@ export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt';
export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions';
export const NEW_AGENT_COMMAND_ID = 'workbench.command.new.agent';
export const NEW_SKILL_COMMAND_ID = 'workbench.command.new.skill';
export const NEW_HOOK_COMMAND_ID = 'workbench.command.new.hook';
class NewPromptFileAction extends AbstractNewPromptFileAction {
constructor() {
@@ -299,168 +288,6 @@ class NewSkillFileAction extends Action2 {
}
}
class NewHookFileAction extends Action2 {
constructor() {
super({
id: NEW_HOOK_COMMAND_ID,
title: localize('commands.new.hook.local.title', "New Hook..."),
f1: false,
precondition: ChatContextKeys.enabled,
category: CHAT_CATEGORY,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib
},
menu: {
id: MenuId.CommandPalette,
when: ChatContextKeys.enabled
}
});
}
public override async run(accessor: ServicesAccessor) {
const editorService = accessor.get(IEditorService);
const fileService = accessor.get(IFileService);
const instaService = accessor.get(IInstantiationService);
const quickInputService = accessor.get(IQuickInputService);
const bulkEditService = accessor.get(IBulkEditService);
const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, PromptsType.hook);
if (!selectedFolder) {
return;
}
// Ask which hook type to add
const hookTypeItems = HOOK_TYPES.map(hookType => ({
id: hookType.id,
label: hookType.label,
description: hookType.description
}));
const selectedHookType = await quickInputService.pick(hookTypeItems, {
placeHolder: localize('commands.new.hook.type.placeholder', "Select a hook type to add"),
title: localize('commands.new.hook.type.title', "Add Hook")
});
if (!selectedHookType) {
return;
}
// Create the hooks folder if it doesn't exist
await fileService.createFolder(selectedFolder.uri);
// Use fixed hooks.json filename
const hookFileUri = URI.joinPath(selectedFolder.uri, HOOKS_FILENAME);
// Check if hooks.json already exists
let hooksContent: { hooks: Record<string, unknown[]> };
const fileExists = await fileService.exists(hookFileUri);
if (fileExists) {
// Parse existing file
const existingContent = await fileService.readFile(hookFileUri);
try {
hooksContent = JSON.parse(existingContent.value.toString());
// Ensure hooks object exists
if (!hooksContent.hooks) {
hooksContent.hooks = {};
}
} catch {
// If parsing fails, show error and open file for user to fix
const notificationService = accessor.get(INotificationService);
notificationService.error(localize('commands.new.hook.parseError', "Failed to parse existing hooks.json. Please fix the JSON syntax errors and try again."));
await editorService.openEditor({ resource: hookFileUri });
return;
}
} else {
// Create new structure
hooksContent = { hooks: {} };
}
// Add the new hook entry (append if hook type already exists)
const hookTypeId = selectedHookType.id as HookType;
const newHookEntry = {
type: 'command',
command: ''
};
let newHookIndex: number;
if (!hooksContent.hooks[hookTypeId]) {
hooksContent.hooks[hookTypeId] = [newHookEntry];
newHookIndex = 0;
} else {
hooksContent.hooks[hookTypeId].push(newHookEntry);
newHookIndex = hooksContent.hooks[hookTypeId].length - 1;
}
// Write the file
const jsonContent = JSON.stringify(hooksContent, null, '\t');
// Check if the file is already open in an editor
const existingEditor = editorService.editors.find(e => isEqual(e.resource, hookFileUri));
if (existingEditor) {
// File is already open - first focus the editor, then update its model directly
await editorService.openEditor({
resource: hookFileUri,
options: {
pinned: false
}
});
// Get the code editor and update its content directly
const editor = getCodeEditor(editorService.activeTextEditorControl);
if (editor && editor.hasModel() && isEqual(editor.getModel().uri, hookFileUri)) {
const model = editor.getModel();
// Apply the full content replacement using executeEdits
model.pushEditOperations([], [{
range: model.getFullModelRange(),
text: jsonContent
}], () => null);
// Find and apply the selection
const selection = findHookCommandSelection(jsonContent, hookTypeId, newHookIndex, 'command');
if (selection && selection.endLineNumber !== undefined && selection.endColumn !== undefined) {
editor.setSelection({
startLineNumber: selection.startLineNumber,
startColumn: selection.startColumn,
endLineNumber: selection.endLineNumber,
endColumn: selection.endColumn
});
editor.revealLineInCenter(selection.startLineNumber);
}
}
} else {
// File is not currently open in an editor
if (!fileExists) {
// File doesn't exist - write new file directly and open
await fileService.writeFile(hookFileUri, VSBuffer.fromString(jsonContent));
} else {
// File exists but isn't open - open it first, then use bulk edit for undo support
await editorService.openEditor({
resource: hookFileUri,
options: { pinned: false }
});
// Apply the edit via bulk edit service for proper undo support
await bulkEditService.apply([
new ResourceTextEdit(hookFileUri, { range: new Range(1, 1, Number.MAX_SAFE_INTEGER, 1), text: jsonContent })
], { label: localize('addHook', "Add Hook") });
}
// Find the selection for the new hook's command field
const selection = findHookCommandSelection(jsonContent, hookTypeId, newHookIndex, 'command');
// Open editor with selection (or re-focus if already open)
await editorService.openEditor({
resource: hookFileUri,
options: {
selection,
pinned: false
}
});
}
}
}
class NewUntitledPromptFileAction extends Action2 {
constructor() {
super({
@@ -506,6 +333,5 @@ export function registerNewPromptFileActions(): void {
registerAction2(NewInstructionsFileAction);
registerAction2(NewAgentFileAction);
registerAction2(NewSkillFileAction);
registerAction2(NewHookFileAction);
registerAction2(NewUntitledPromptFileAction);
}

View File

@@ -903,7 +903,7 @@ export class ChatService extends Disposable implements IChatService {
let detectedAgent: IChatAgentData | undefined;
let detectedCommand: IChatAgentCommand | undefined;
// Collect hooks from hooks.json files
// Collect hooks from hook .json files
let collectedHooks: IChatRequestHooks | undefined;
try {
collectedHooks = await this.promptsService.getHooks(token);

View File

@@ -153,7 +153,7 @@ export interface IChatAgentRequest {
editedFileEvents?: IChatAgentEditedFileEvent[];
/**
* Collected hooks configuration for this request.
* Contains all hooks defined in hooks.json files, organized by hook type.
* Contains all hooks defined in hooks .json files, organized by hook type.
*/
hooks?: IChatRequestHooks;
/**

View File

@@ -55,11 +55,6 @@ export const CLAUDE_LOCAL_MD_FILENAME = 'CLAUDE.local.md';
*/
export const CLAUDE_CONFIG_FOLDER = '.claude';
/**
* Default hook file name (case insensitive).
*/
export const HOOKS_FILENAME = 'hooks.json';
/**
* Copilot custom instructions file name.
*/
@@ -185,9 +180,10 @@ export const DEFAULT_AGENT_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [
/**
* Default hook file paths.
* Entries can be either a directory or a specific file path (.json)
*/
export const DEFAULT_HOOK_FILE_PATHS: readonly IPromptSourceFolder[] = [
{ path: '.github/hooks/hooks.json', source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local },
{ path: '.github/hooks', source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local },
{ path: '.claude/settings.local.json', source: PromptFileSource.ClaudeWorkspaceLocal, storage: PromptsStorage.local },
{ path: '.claude/settings.json', source: PromptFileSource.ClaudeWorkspace, storage: PromptsStorage.local },
{ path: '~/.claude/settings.json', source: PromptFileSource.ClaudePersonal, storage: PromptsStorage.user },
@@ -203,6 +199,11 @@ function isInAgentsFolder(fileUri: URI): boolean {
/**
* Gets the prompt file type from the provided path.
*
* Note: This function assumes the URI is already known to be a prompt file
* (e.g., from a configured prompt source folder). It does not validate that
* arbitrary URIs are prompt files - for example, any .json file will return
* PromptsType.hook regardless of its location.
*/
export function getPromptFileType(fileUri: URI): PromptsType | undefined {
const filename = basename(fileUri.path);
@@ -229,18 +230,12 @@ export function getPromptFileType(fileUri: URI): PromptsType | undefined {
return PromptsType.agent;
}
// Check if it's a hooks.json file (case insensitive)
if (filename.toLowerCase() === HOOKS_FILENAME.toLowerCase()) {
// Any .json file is treated as a hook file.
// The caller is responsible for only passing URIs from valid prompt source folders.
if (filename.toLowerCase().endsWith('.json')) {
return PromptsType.hook;
}
// Check if it's a settings.local.json or settings.json file in a .claude folder
if (filename.toLowerCase() === 'settings.local.json' || filename.toLowerCase() === 'settings.json') {
const dir = dirname(fileUri.path);
if (basename(dir) === CLAUDE_CONFIG_FOLDER) {
return PromptsType.hook;
}
}
return undefined;
}
@@ -262,7 +257,7 @@ export function getPromptFileExtension(type: PromptsType): string {
case PromptsType.skill:
return SKILL_FILENAME;
case PromptsType.hook:
return HOOKS_FILENAME;
return '.json';
default:
throw new Error('Unknown prompt type');
}

View File

@@ -30,7 +30,7 @@ export interface IResolvedHookEntry {
* Supported hook file formats.
*/
export enum HookSourceFormat {
/** GitHub Copilot hooks.json format */
/** GitHub Copilot hooks .json format */
Copilot = 'copilot',
/** Claude settings.json / settings.local.json format */
Claude = 'claude',
@@ -48,11 +48,6 @@ export function getHookSourceFormat(fileUri: URI): HookSourceFormat {
return HookSourceFormat.Claude;
}
// Copilot format: hooks.json (typically in .github/hooks/)
if (filename === 'hooks.json') {
return HookSourceFormat.Copilot;
}
// Default to Copilot format
return HookSourceFormat.Copilot;
}
@@ -66,7 +61,7 @@ export function isReadOnlyHookSource(format: HookSourceFormat): boolean {
}
/**
* Parses hooks from a Copilot hooks.json file (our native format).
* Parses hooks from a Copilot hooks .json file (our native format).
*/
export function parseCopilotHooks(
json: unknown,

View File

@@ -12,7 +12,7 @@ import { untildify } from '../../../../../base/common/labels.js';
import { OperatingSystem } from '../../../../../base/common/platform.js';
/**
* Enum of available hook types that can be configured in hooks.json
* Enum of available hook types that can be configured in hooks .json
*/
export enum HookType {
SessionStart = 'SessionStart',
@@ -37,7 +37,7 @@ export const HOOK_TYPES = [
{
id: HookType.SessionStart,
label: nls.localize('hookType.sessionStart.label', "Session Start"),
description: nls.localize('hookType.sessionStart.description', "Executed when a new agent session begins or when resuming an existing session.")
description: nls.localize('hookType.sessionStart.description', "Executed when a new agent session begins.")
},
{
id: HookType.UserPromptSubmit,
@@ -191,7 +191,7 @@ export const hookFileSchema: IJSONSchema = {
properties: {
SessionStart: {
...hookArraySchema,
description: nls.localize('hookFile.sessionStart', 'Executed when a new agent session begins or when resuming an existing session. Use to initialize environments, log session starts, validate project state, or set up temporary resources.')
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,
@@ -257,7 +257,7 @@ export const HOOK_SCHEMA_URI = 'vscode://schemas/hooks';
/**
* Glob pattern for hook files.
*/
export const HOOK_FILE_GLOB = 'hooks/hooks.json';
export const HOOK_FILE_GLOB = '.github/hooks/*.json';
/**
* Normalizes a raw hook type identifier to the canonical HookType enum value.

View File

@@ -12,7 +12,7 @@ import { getPromptFileLocationsConfigKey, isTildePath, PromptsConfig } from '../
import { basename, dirname, isEqualOrParent, joinPath } from '../../../../../../base/common/resources.js';
import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, DEFAULT_AGENT_SOURCE_FOLDERS, DEFAULT_HOOK_FILE_PATHS, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource, HOOKS_SOURCE_FOLDER } from '../config/promptFileLocations.js';
import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, DEFAULT_AGENT_SOURCE_FOLDERS, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js';
import { PromptsType } from '../promptTypes.js';
import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js';
import { Schemas } from '../../../../../../base/common/network.js';
@@ -88,8 +88,8 @@ export class PromptFilesLocator {
// Filter to only user storage locations
const result = absoluteLocations.filter(loc => loc.storage === PromptsStorage.user);
// Also include the VS Code user data prompts folder (for all types except skills)
if (type !== PromptsType.skill) {
// Also include the VS Code user data prompts folder for certain types
if (type === PromptsType.agent || type === PromptsType.instructions || type === PromptsType.prompt) {
const userDataPromptsHome = this.userDataService.currentProfile.promptsHome;
return [
...result,
@@ -113,22 +113,28 @@ export class PromptFilesLocator {
private getSourceFoldersSync(type: PromptsType, userHome: URI): readonly URI[] {
const result: URI[] = [];
const { folders } = this.workspaceService.getWorkspace();
const defaultFolders = getPromptFileDefaultLocations(type);
const defaultFileOrFolders = getPromptFileDefaultLocations(type);
for (const sourceFolder of defaultFolders) {
let folderPath: URI;
if (sourceFolder.storage === PromptsStorage.local) {
const getFolderUri = (type: PromptsType, fileOrFolderPath: URI): URI => {
// For hooks, the paths are sometimes file paths, so get the parent directory in that case
if (type === PromptsType.hook && fileOrFolderPath.path.toLowerCase().endsWith('.json')) {
return dirname(fileOrFolderPath);
}
return fileOrFolderPath;
};
for (const sourceFileOrFolder of defaultFileOrFolders) {
let fileOrFolderPath: URI;
if (sourceFileOrFolder.storage === PromptsStorage.local) {
for (const workspaceFolder of folders) {
folderPath = joinPath(workspaceFolder.uri, sourceFolder.path);
// For hooks, the paths are file paths, so get the parent directory
result.push(type === PromptsType.hook ? dirname(folderPath) : folderPath);
fileOrFolderPath = joinPath(workspaceFolder.uri, sourceFileOrFolder.path);
result.push(getFolderUri(type, fileOrFolderPath));
}
} else if (sourceFolder.storage === PromptsStorage.user) {
} else if (sourceFileOrFolder.storage === PromptsStorage.user) {
// For tilde paths, strip the ~/ prefix before joining with userHome
const relativePath = isTildePath(sourceFolder.path) ? sourceFolder.path.substring(2) : sourceFolder.path;
folderPath = joinPath(userHome, relativePath);
// For hooks, the paths are file paths, so get the parent directory
result.push(type === PromptsType.hook ? dirname(folderPath) : folderPath);
const relativePath = isTildePath(sourceFileOrFolder.path) ? sourceFileOrFolder.path.substring(2) : sourceFileOrFolder.path;
fileOrFolderPath = joinPath(userHome, relativePath);
result.push(getFolderUri(type, fileOrFolderPath));
}
}
@@ -202,11 +208,36 @@ export class PromptFilesLocator {
/**
* Gets the hook source folders for creating new hooks.
* Returns only the Copilot hooks folder (.github/hooks) since Claude paths are read-only.
* Returns folders from config, excluding user storage and Claude paths (which are read-only).
*/
public async getHookSourceFolders(): Promise<readonly URI[]> {
const { folders } = this.workspaceService.getWorkspace();
return folders.map(folder => joinPath(folder.uri, HOOKS_SOURCE_FOLDER));
const userHome = await this.pathService.userHome();
const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, PromptsType.hook);
// Ignore claude folders since they aren't first-class supported, so we don't want to create invalid formats
// Check for .claude as an actual path segment (starts with ".claude/" or contains "/.claude/")
const allowedHookFolders = configuredLocations.filter(loc =>
!loc.path.startsWith('.claude/') && !loc.path.includes('/.claude/')
);
// Convert to absolute URIs
const result = new ResourceSet();
const absoluteLocations = this.toAbsoluteLocations(PromptsType.hook, allowedHookFolders, userHome);
for (const location of absoluteLocations) {
// For hook configs, entries are directories unless the path ends with .json (specific file)
// Default entries have filePattern, user entries don't but are still directories
const isSpecificFile = location.uri.path.endsWith('.json');
if (isSpecificFile) {
// It's a specific file path (like .github/hooks/hooks.json), use parent directory
result.add(dirname(location.uri));
} else {
// It's a directory path (like .github/hooks or .github/books)
result.add(location.uri);
}
}
return [...result];
}
/**
@@ -342,20 +373,7 @@ export class PromptFilesLocator {
private getLocalParentFolders(type: PromptsType): readonly { parent: URI; filePattern?: string }[] {
const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type);
if (type === PromptsType.agent) {
configuredLocations.push(...DEFAULT_AGENT_SOURCE_FOLDERS);
} else if (type === PromptsType.hook) {
configuredLocations.push(...DEFAULT_HOOK_FILE_PATHS);
}
const absoluteLocations = this.toAbsoluteLocations(type, configuredLocations, undefined);
// For hooks, the paths are file paths (e.g., '.github/hooks/hooks.json'), not folder paths.
// We need to watch the parent directories of these files.
if (type === PromptsType.hook) {
return absoluteLocations.map((location) => ({ parent: dirname(location.uri) }));
}
return absoluteLocations.map((location) => firstNonGlobParentAndPattern(location.uri));
}

View File

@@ -29,7 +29,7 @@ import { TestContextService, TestUserDataProfileService } from '../../../../../t
import { ChatRequestVariableSet, isPromptFileVariableEntry, isPromptTextVariableEntry, toFileVariableEntry } from '../../../common/attachments/chatVariableEntries.js';
import { ComputeAutomaticInstructions } from '../../../common/promptSyntax/computeAutomaticInstructions.js';
import { PromptsConfig } from '../../../common/promptSyntax/config/config.js';
import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/config/promptFileLocations.js';
import { AGENTS_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/config/promptFileLocations.js';
import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/promptTypes.js';
import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js';
import { PromptsService } from '../../../common/promptSyntax/service/promptsServiceImpl.js';
@@ -73,6 +73,7 @@ suite('ComputeAutomaticInstructions', () => {
testConfigService.setUserConfiguration(PromptsConfig.PROMPT_LOCATIONS_KEY, { [PROMPT_DEFAULT_SOURCE_FOLDER]: true });
testConfigService.setUserConfiguration(PromptsConfig.MODE_LOCATION_KEY, { [LEGACY_MODE_DEFAULT_SOURCE_FOLDER]: true });
testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { '.claude/skills': true });
testConfigService.setUserConfiguration(PromptsConfig.AGENTS_LOCATION_KEY, { [AGENTS_SOURCE_FOLDER]: true });
instaService.stub(IConfigurationService, testConfigService);
instaService.stub(IUserDataProfileService, new TestUserDataProfileService());

View File

@@ -83,50 +83,25 @@ suite('promptFileLocations', function () {
assert.strictEqual(getPromptFileType(uri), PromptsType.skill);
});
test('hooks.json should be recognized as hook', () => {
const uri = URI.file('/workspace/.github/hooks/hooks.json');
assert.strictEqual(getPromptFileType(uri), PromptsType.hook);
// Note: getPromptFileType assumes the URI is from a valid prompt source folder.
// Any .json file returns PromptsType.hook - the caller filters by folder.
test('any .json file should be recognized as hook', () => {
assert.strictEqual(getPromptFileType(URI.file('/workspace/.github/hooks/hooks.json')), PromptsType.hook);
assert.strictEqual(getPromptFileType(URI.file('/workspace/.github/hooks/custom-hooks.json')), PromptsType.hook);
assert.strictEqual(getPromptFileType(URI.file('/workspace/.claude/settings.json')), PromptsType.hook);
assert.strictEqual(getPromptFileType(URI.file('/workspace/.claude/settings.local.json')), PromptsType.hook);
assert.strictEqual(getPromptFileType(URI.file('/workspace/any/path/config.json')), PromptsType.hook);
});
test('HOOKS.JSON (uppercase) should be recognized as hook', () => {
const uri = URI.file('/workspace/.github/hooks/HOOKS.JSON');
assert.strictEqual(getPromptFileType(uri), PromptsType.hook);
test('.json files are case insensitive', () => {
assert.strictEqual(getPromptFileType(URI.file('/workspace/.github/hooks/HOOKS.JSON')), PromptsType.hook);
assert.strictEqual(getPromptFileType(URI.file('/workspace/.claude/SETTINGS.JSON')), PromptsType.hook);
});
test('hooks.json in any folder should be recognized as hook', () => {
const uri = URI.file('/workspace/some/other/path/hooks.json');
assert.strictEqual(getPromptFileType(uri), PromptsType.hook);
});
test('settings.json in .claude folder should be recognized as hook', () => {
const uri = URI.file('/workspace/.claude/settings.json');
assert.strictEqual(getPromptFileType(uri), PromptsType.hook);
});
test('settings.local.json in .claude folder should be recognized as hook', () => {
const uri = URI.file('/workspace/.claude/settings.local.json');
assert.strictEqual(getPromptFileType(uri), PromptsType.hook);
});
test('SETTINGS.JSON (uppercase) in .claude folder should be recognized as hook', () => {
const uri = URI.file('/workspace/.claude/SETTINGS.JSON');
assert.strictEqual(getPromptFileType(uri), PromptsType.hook);
});
test('settings.json outside .claude folder should NOT be recognized as hook', () => {
const uri = URI.file('/workspace/.github/settings.json');
test('non-json file in .github/hooks folder should NOT be recognized as hook', () => {
const uri = URI.file('/workspace/.github/hooks/readme.md');
assert.strictEqual(getPromptFileType(uri), undefined);
});
test('settings.local.json outside .claude folder should NOT be recognized as hook', () => {
const uri = URI.file('/workspace/some/path/settings.local.json');
assert.strictEqual(getPromptFileType(uri), undefined);
});
test('settings.json in ~/.claude folder should be recognized as hook', () => {
const uri = URI.file('/Users/user/.claude/settings.json');
assert.strictEqual(getPromptFileType(uri), PromptsType.hook);
});
});
suite('getCleanPromptName', () => {
@@ -208,20 +183,13 @@ suite('promptFileLocations', function () {
assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/SKILL2.md')), false);
});
test('hooks.json should return true', () => {
assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/.github/hooks/hooks.json')), true);
});
test('settings.json in .claude folder should return true', () => {
// Note: Any .json file returns true because getPromptFileType returns hook for all JSON.
// The caller is responsible for only passing URIs from valid prompt source folders.
test('any .json file should return true', () => {
assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/.github/hooks/custom-hooks.json')), true);
assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/.claude/settings.json')), true);
});
test('settings.local.json in .claude folder should return true', () => {
assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/.claude/settings.local.json')), true);
});
test('settings.json outside .claude folder should return false', () => {
assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/settings.json')), false);
assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/settings.json')), true);
});
});
});

View File

@@ -38,7 +38,7 @@ import { TestContextService, TestUserDataProfileService } from '../../../../../.
import { ChatRequestVariableSet, isPromptFileVariableEntry, toFileVariableEntry } from '../../../../common/attachments/chatVariableEntries.js';
import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '../../../../common/promptSyntax/computeAutomaticInstructions.js';
import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js';
import { CLAUDE_CONFIG_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js';
import { AGENTS_SOURCE_FOLDER, CLAUDE_CONFIG_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js';
import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js';
import { ExtensionAgentSourceType, ICustomAgent, IPromptFileContext, IPromptsService, PromptsStorage, Target } from '../../../../common/promptSyntax/service/promptsService.js';
import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js';
@@ -74,6 +74,7 @@ suite('PromptsService', () => {
testConfigService.setUserConfiguration(PromptsConfig.INSTRUCTIONS_LOCATION_KEY, { [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true });
testConfigService.setUserConfiguration(PromptsConfig.PROMPT_LOCATIONS_KEY, { [PROMPT_DEFAULT_SOURCE_FOLDER]: true });
testConfigService.setUserConfiguration(PromptsConfig.MODE_LOCATION_KEY, { [LEGACY_MODE_DEFAULT_SOURCE_FOLDER]: true });
testConfigService.setUserConfiguration(PromptsConfig.AGENTS_LOCATION_KEY, { [AGENTS_SOURCE_FOLDER]: true });
instaService.stub(IConfigurationService, testConfigService);
instaService.stub(IWorkbenchEnvironmentService, {});

View File

@@ -63,7 +63,7 @@ declare module 'vscode' {
export namespace chat {
/**
* Execute all hooks of the specified type for the current chat session.
* Hooks are configured in hooks.json files in the workspace.
* Hooks are configured in hooks .json files in the workspace.
*
* @param hookType The type of hook to execute.
* @param options Hook execution options including the input data.