Path: blob/main/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts
5243 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { URI } from '../../../../../base/common/uri.js';6import { basename, dirname } from '../../../../../base/common/path.js';7import { HookType, IHookCommand, toHookType, resolveHookCommand } from './hookSchema.js';8import { parseClaudeHooks } from './hookClaudeCompat.js';9import { resolveCopilotCliHookType } from './hookCopilotCliCompat.js';1011/**12* Represents a hook source with its original and normalized properties.13* Used to display hooks from different formats in a unified view.14*/15export interface IResolvedHookEntry {16/** The normalized hook type (our canonical HookType enum) */17readonly hookType: HookType;18/** The original hook type ID as it appears in the source file */19readonly originalHookTypeId: string;20/** The source format this hook came from */21readonly sourceFormat: HookSourceFormat;22/** The resolved hook command */23readonly command: IHookCommand;24/** The index of this hook in its array (for editing) */25readonly index: number;26}2728/**29* Supported hook file formats.30*/31export enum HookSourceFormat {32/** GitHub Copilot hooks .json format */33Copilot = 'copilot',34/** Claude settings.json / settings.local.json format */35Claude = 'claude',36}3738/**39* Determines the hook source format based on the file URI.40*/41export function getHookSourceFormat(fileUri: URI): HookSourceFormat {42const filename = basename(fileUri.path).toLowerCase();43const dir = dirname(fileUri.path);4445// Claude format: .claude/settings.json or .claude/settings.local.json46if ((filename === 'settings.json' || filename === 'settings.local.json') && dir.endsWith('.claude')) {47return HookSourceFormat.Claude;48}4950// Default to Copilot format51return HookSourceFormat.Copilot;52}5354/**55* Checks if a file is read-only based on its source format.56* Claude settings files should be read-only from our perspective since they have a different format.57*/58export function isReadOnlyHookSource(format: HookSourceFormat): boolean {59return format === HookSourceFormat.Claude;60}6162/**63* Parses hooks from a Copilot hooks .json file (our native format).64*/65export function parseCopilotHooks(66json: unknown,67workspaceRootUri: URI | undefined,68userHome: string69): Map<HookType, { hooks: IHookCommand[]; originalId: string }> {70const result = new Map<HookType, { hooks: IHookCommand[]; originalId: string }>();7172if (!json || typeof json !== 'object') {73return result;74}7576const root = json as Record<string, unknown>;7778const hooks = root.hooks;79if (!hooks || typeof hooks !== 'object') {80return result;81}8283const hooksObj = hooks as Record<string, unknown>;8485for (const originalId of Object.keys(hooksObj)) {86const hookType = resolveCopilotCliHookType(originalId) ?? toHookType(originalId);87if (!hookType) {88continue;89}9091const hookArray = hooksObj[originalId];92if (!Array.isArray(hookArray)) {93continue;94}9596const commands: IHookCommand[] = [];9798for (const item of hookArray) {99const resolved = resolveHookCommand(item as Record<string, unknown>, workspaceRootUri, userHome);100if (resolved) {101commands.push(resolved);102}103}104105if (commands.length > 0) {106result.set(hookType, { hooks: commands, originalId });107}108}109110return result;111}112113/**114* Parses hooks from any supported format, auto-detecting the format from the file URI.115*/116export function parseHooksFromFile(117fileUri: URI,118json: unknown,119workspaceRootUri: URI | undefined,120userHome: string121): { format: HookSourceFormat; hooks: Map<HookType, { hooks: IHookCommand[]; originalId: string }> } {122const format = getHookSourceFormat(fileUri);123124let hooks: Map<HookType, { hooks: IHookCommand[]; originalId: string }>;125126switch (format) {127case HookSourceFormat.Claude:128hooks = parseClaudeHooks(json, workspaceRootUri, userHome);129break;130case HookSourceFormat.Copilot:131default:132hooks = parseCopilotHooks(json, workspaceRootUri, userHome);133break;134}135136return { format, hooks };137}138139/**140* Gets a human-readable label for a hook source format.141*/142export function getHookSourceFormatLabel(format: HookSourceFormat): string {143switch (format) {144case HookSourceFormat.Claude:145return 'Claude';146case HookSourceFormat.Copilot:147return 'GitHub Copilot';148}149}150151152