hookSchema.ts ×15

Frontier kind: Code frontier

unlabeled · c_93108f15a7eb

496 tests · 14563 LOC · 56 files · introduces 0 tests · 385 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
15 ranges385 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1122 ranges14563 lines · 56 files · Browse complete extent
All tests (intent)
496 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 385 introduced LOC across 15 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts 385 introduced LOC · 15 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- hookSchema.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { IJSONSchema } from '../../../../../base/common/jsonSchema.js';
7 > import * as nls from '../../../../../nls.js';
8 > import { URI } from '../../../../../base/common/uri.js';
9 > import { joinPath } from '../../../../../base/common/resources.js';
10 > import { isAbsolute } from '../../../../../base/common/path.js';
11 > import { untildify } from '../../../../../base/common/labels.js';
12 > import { OperatingSystem } from '../../../../../base/common/platform.js';
13 > import { IParsedHookCommand } from '../../../../../platform/agentPlugins/common/pluginParsers.js';
14 > import { HookType, HOOKS_BY_TARGET, HOOK_METADATA } from './hookTypes.js';
15 > import { Target } from './promptTypes.js';
16 > import { IValue, IMapValue } from './promptFileParser.js';
17 >
18 > /**
19 > * A single hook command configuration.
20 > * Extends the platform-layer {@link IParsedHookCommand} with editor-specific
21 > * metadata used for UI display and field highlighting.
22 > */
23 > export interface IHookCommand extends IParsedHookCommand {
24 > readonly type: 'command';
25 > /** Original JSON field name that provided the windows command. */
26 > readonly windowsSource?: 'windows' | 'powershell';
27 > /** Original JSON field name that provided the linux command. */
28 > readonly linuxSource?: 'linux' | 'bash';
29 > /** Original JSON field name that provided the osx command. */
30 > readonly osxSource?: 'osx' | 'bash';
31 > }
32 >
33 > /**
34 > * Collected hooks for a chat request, organized by hook type.
35 > * This is passed to the extension host so it knows what hooks are available.
36 > */
37 > export type ChatRequestHooks = {
38 > readonly [K in HookType]?: readonly IParsedHookCommand[];
39 > };
40 >
41 > export namespace ChatRequestHooks {
42 > export function isEquals(a: ChatRequestHooks | undefined, b: ChatRequestHooks | undefined): boolean {
43 if (a === b) {
44 return true;
63 return true;
64 }
65 > } hookSchema.ts
66 >
67 > /**
68 > * Merges two sets of hooks by concatenating the command arrays for each hook type.
69 > * Additional hooks are appended after the base hooks.
70 > */
71 > export function mergeHooks(base: ChatRequestHooks | undefined, additional: ChatRequestHooks): ChatRequestHooks {
72 if (!base) {
73 return additional;
84 return result as ChatRequestHooks;
85 }
87 > /**
88 > * Descriptions for hook command fields, used by both the JSON schema and the hover provider.
89 > */
90 > export const HOOK_COMMAND_FIELD_DESCRIPTIONS: Record<string, string> = {
91 > type: nls.localize('hook.type', 'Must be "command".'),
92 > command: nls.localize('hook.command', 'The command to execute. This is the default cross-platform command.'),
93 > windows: nls.localize('hook.windows', 'Windows-specific command. If specified and running on Windows, this overrides the "command" field.'),
94 > linux: nls.localize('hook.linux', 'Linux-specific command. If specified and running on Linux, this overrides the "command" field.'),
95 > osx: nls.localize('hook.osx', 'macOS-specific command. If specified and running on macOS, this overrides the "command" field.'),
96 > bash: nls.localize('hook.bash', 'Bash command for Linux and macOS.'),
97 > powershell: nls.localize('hook.powershell', 'PowerShell command for Windows.'),
98 > cwd: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).'),
99 > env: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.'),
100 > timeout: nls.localize('hook.timeout', 'Maximum execution time in seconds (default: 30).'),
101 > timeoutSec: nls.localize('hook.timeoutSec', 'Maximum execution time in seconds (default: 10).'),
102 > };
103 >
104 > /**
105 > * JSON Schema for GitHub Copilot hook configuration files.
106 > * Hooks enable executing custom shell commands at strategic points in an agent's workflow.
107 > */
108 > const vscodeHookCommandSchema: IJSONSchema = {
109 > type: 'object',
110 > additionalProperties: true,
111 > required: ['type'],
112 > anyOf: [
113 > { required: ['command'] },
114 > { required: ['windows'] },
115 > { required: ['linux'] },
116 > { required: ['osx'] },
117 > { required: ['bash'] },
118 > { required: ['powershell'] }
119 > ],
120 > errorMessage: nls.localize('hook.commandRequired', 'At least one of "command", "windows", "linux", or "osx" must be specified.'),
121 > properties: {
122 > type: {
123 > type: 'string',
124 > enum: ['command'],
125 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.type
126 > },
127 > command: {
128 > type: 'string',
129 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.command
130 > },
131 > windows: {
132 > type: 'string',
133 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.windows
134 > },
135 > linux: {
136 > type: 'string',
137 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.linux
138 > },
139 > osx: {
140 > type: 'string',
141 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.osx
142 > },
143 > cwd: {
144 > type: 'string',
145 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.cwd
146 > },
147 > env: {
148 > type: 'object',
149 > additionalProperties: { type: 'string' },
150 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.env
151 > },
152 > timeout: {
153 > type: 'number',
154 > default: 30,
155 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.timeout
156 > }
157 > }
158 > };
159 >
160 > const hookArraySchema: IJSONSchema = {
161 > type: 'array',
162 > items: vscodeHookCommandSchema
163 > };
164 >
165 > /**
166 > * Builds JSON Schema hook properties for a given target by looking up
167 > * the hook keys from HOOKS_BY_TARGET and descriptions from HOOK_METADATA.
168 > */
169 > function buildHookProperties(target: Target, arraySchema: IJSONSchema): Record<string, IJSONSchema> {
170 > return Object.fromEntries(
171 > Object.entries(HOOKS_BY_TARGET[target]).map(([key, hookType]) => [
172 > key,
173 > { ...arraySchema, description: HOOK_METADATA[hookType]?.description }
174 > ])
175 > );
176 > }
177 >
178 > /**
179 > * Hook properties for the VS Code format.
180 > */
181 > const vscodeHookProperties: Record<string, IJSONSchema> = buildHookProperties(Target.VSCode, hookArraySchema);
182 >
183 > /**
184 > * Hook command schema for the Copilot CLI format.
185 > * Adds `bash`, `powershell`, and `timeoutSec` fields alongside the standard ones.
186 > */
187 > const copilotCliHookCommandSchema: IJSONSchema = {
188 > type: 'object',
189 > additionalProperties: true,
190 > required: ['type'],
191 > anyOf: [
192 > { required: ['bash'] },
193 > { required: ['powershell'] }
194 > ],
195 > errorMessage: nls.localize('hook.cliCommandRequired', 'At least one of "bash" or "powershell" must be specified.'),
196 > properties: {
197 > type: {
198 > type: 'string',
199 > enum: ['command'],
200 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.type
201 > },
202 > bash: {
203 > type: 'string',
204 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.bash
205 > },
206 > powershell: {
207 > type: 'string',
208 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.powershell
209 > },
210 > cwd: {
211 > type: 'string',
212 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.cwd
213 > },
214 > env: {
215 > type: 'object',
216 > additionalProperties: { type: 'string' },
217 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.env
218 > },
219 > timeoutSec: {
220 > type: 'number',
221 > default: 10,
222 > description: HOOK_COMMAND_FIELD_DESCRIPTIONS.timeoutSec
223 > }
224 > }
225 > };
226 >
227 > const copilotCliHookArraySchema: IJSONSchema = {
228 > type: 'array',
229 > items: copilotCliHookCommandSchema
230 > };
231 >
232 > /**
233 > * Hook properties for the Copilot CLI format.
234 > */
235 > const copilotCliHookProperties: Record<string, IJSONSchema> = buildHookProperties(Target.GitHubCopilot, copilotCliHookArraySchema);
236 >
237 > export const hookFileSchema: IJSONSchema = {
238 > $schema: 'http://json-schema.org/draft-07/schema#',
239 > type: 'object',
240 > description: nls.localize('hookFile.description', 'GitHub Copilot hook configuration file. Hooks enable executing custom shell commands at strategic points in an agent\'s workflow.'),
241 > additionalProperties: true,
242 > required: ['hooks'],
243 > properties: {
244 > hooks: {
245 > type: 'object',
246 > description: nls.localize('hookFile.hooks', 'Hook definitions organized by type.'),
247 > additionalProperties: true,
248 > }
249 > },
250 > // Conditionally apply PascalCase or camelCase hook properties based on
251 > // whether the file uses the Copilot CLI format (detected by the "version" field).
252 > if: {
253 > required: ['version'],
254 > properties: {
255 > version: { type: 'number' }
256 > }
257 > },
258 > then: {
259 > // Copilot CLI format: camelCase hook names, bash/powershell/timeoutSec fields
260 > properties: {
261 > version: {
262 > type: 'number',
263 > description: nls.localize('hookFile.version', 'Hook configuration format version.'),
264 > },
265 > hooks: {
266 > properties: copilotCliHookProperties
267 > }
268 > }
269 > },
270 > else: {
271 > // VS Code / PascalCase format
272 > properties: {
273 > hooks: {
274 > properties: vscodeHookProperties
275 > }
276 > }
277 > },
278 > defaultSnippets: [
279 > {
280 > label: nls.localize('hookFile.snippet.basic', 'Basic hook configuration'),
281 > description: nls.localize('hookFile.snippet.basic.description', 'A basic hook configuration with common hooks'),
282 > body: {
283 > hooks: {
284 > SessionStart: [
285 > {
286 > type: 'command',
287 > command: '${1:echo "Session started" >> session.log}',
288 > }
289 > ],
290 > PreToolUse: [
291 > {
292 > type: 'command',
293 > command: '${2:./scripts/validate.sh}',
294 > timeout: 15
295 > }
296 > ]
297 > }
298 > }
299 > }
300 > ]
301 > };
302 >
303 > /**
304 > * URI for the hook schema registration.
305 > */
306 > export const HOOK_SCHEMA_URI = 'vscode://schemas/hooks';
307 >
308 > /**
309 > * Normalizes a raw hook type identifier to the canonical HookType enum value.
310 > * Only matches exact enum values. For tool-specific naming conventions (e.g., Claude, Copilot CLI),
311 > * use the corresponding compat module's resolver function.
312 > */
313 > export function toHookType(rawHookTypeId: string): HookType | undefined {
314 if (Object.values(HookType).includes(rawHookTypeId as HookType)) {
315 return rawHookTypeId as HookType;
317 return undefined;
318 }
320 > /**
321 > * Normalizes a raw hook command object, validating structure.
322 > * Maps legacy bash/powershell fields to platform-specific overrides:
323 > * - bash -> linux + osx
324 > * - powershell -> windows
325 > * This is an internal helper - use resolveHookCommand for the full resolution.
326 > */
327 function normalizeHookCommand(raw: Record<string, unknown>): { command?: string; windows?: string; linux?: string; osx?: string; windowsSource?: 'windows' | 'powershell'; linuxSource?: 'linux' | 'bash'; osxSource?: 'osx' | 'bash'; cwd?: string; env?: Record<string, string>; timeout?: number } | undefined {
328 if (raw.type !== 'command') {
364 };
365 }
367 > /**
368 > * Gets a label for the given platform.
369 > */
370 > export function getPlatformLabel(os: OperatingSystem): string {
371 if (os === OperatingSystem.Windows) {
372 return 'Windows';
378 return '';
379 }
381 > /**
382 > * Resolves the effective command for the given platform.
383 > * This applies OS-specific overrides (windows, linux, osx) to get the actual command that will be executed.
384 > * Similar to how launch.json handles platform-specific configurations in debugAdapter.ts.
385 > */
386 > export function resolveEffectiveCommand(hook: IParsedHookCommand, os: OperatingSystem): string | undefined {
387 // Select the platform-specific override based on the OS
388 if (os === OperatingSystem.Windows && hook.windows) {
397 return hook.command;
398 }
400 > /**
401 > * Checks if the hook is using a platform-specific command override.
402 > */
403 > export function isUsingPlatformOverride(hook: IParsedHookCommand, os: OperatingSystem): boolean {
404 if (os === OperatingSystem.Windows && hook.windows) {
405 return true;
411 return false;
412 }
414 > /**
415 > * Gets the source shell type for the effective command on the given platform.
416 > * Returns 'powershell' if the Windows command came from a powershell field,
417 > * 'bash' if the Linux/macOS command came from a bash field,
418 > * or undefined for default shell handling.
419 > */
420 > export function getEffectiveCommandSource(hook: IHookCommand, os: OperatingSystem): 'powershell' | 'bash' | undefined {
421 if (os === OperatingSystem.Windows && hook.windows && hook.windowsSource === 'powershell') {
422 return 'powershell';
428 return undefined;
429 }
431 > /**
432 > * Gets the original JSON field key name for the given platform's command.
433 > * Returns the actual field name from the JSON (e.g., 'bash' instead of 'osx' if bash was used).
434 > * This is used for editor focus to highlight the correct field.
435 > */
436 > export function getEffectiveCommandFieldKey(hook: IHookCommand | IParsedHookCommand, os: OperatingSystem): string {
437 const h = hook as Partial<IHookCommand>;
438 if (os === OperatingSystem.Windows && hook.windows) {
445 return 'command';
446 }
448 > /**
449 > * Formats a hook command for display.
450 > * Resolves OS-specific overrides to show the effective command for the given platform.
451 > * If using a platform-specific override, includes the platform as a prefix badge.
452 > */
453 > export function formatHookCommandLabel(hook: IParsedHookCommand, os: OperatingSystem): string {
454 const command = resolveEffectiveCommand(hook, os);
455 if (!command) {
458 return command;
459 }
461 > /**
462 > * Resolves a raw hook command object to the canonical IHookCommand format.
463 > * Normalizes the command and resolves the cwd path relative to the workspace root.
464 > * @param raw The raw hook command object from JSON
465 > * @param workspaceRootUri The workspace root URI to resolve relative cwd paths against
466 > * @param userHome The user's home directory path for tilde expansion
467 > */
468 > export function resolveHookCommand(raw: Record<string, unknown>, workspaceRootUri: URI | undefined, userHome: string): IHookCommand | undefined {
469 const normalized = normalizeHookCommand(raw);
470 if (!normalized) {
501 };
502 }
504 > /**
505 > * Helper to extract hook commands from an item that could be:
506 > * 1. A direct command object: { type: 'command', command: '...' }
507 > * 2. A nested structure with matcher (Claude style): { matcher: '...', hooks: [{ type: 'command', command: '...' }] }
508 > *
509 > * This allows Copilot format to handle Claude-style entries if pasted.
510 > * Also handles Claude's leniency where 'type' field can be omitted.
511 > */
512 > export function extractHookCommandsFromItem(
513 item: unknown,
514 workspaceRootUri: URI | undefined,
546 return commands;
547 }
549 > /**
550 > * Normalizes a hook command object for resolving.
551 > * Claude format allows omitting the 'type' field, treating it as 'command'.
552 > * This ensures compatibility when Claude-style hooks are pasted into Copilot format.
553 > */
554 function normalizeForResolve(raw: Record<string, unknown>): Record<string, unknown> {
555 // If type is missing or already 'command', ensure it's set to 'command'
559 return raw;
560 }
562 > /**
563 > * Converts an {@link IValue} YAML AST node into a plain JavaScript value
564 > * (string, array, or object) suitable for passing to hook parsing helpers.
565 > */
566 function yamlValueToPlain(value: IValue): unknown {
567 switch (value.type) {
579 }
580 }
582 > /**
583 > * Parses hooks from a subagent's YAML frontmatter `hooks` attribute.
584 > *
585 > * Supports two formats for hook entries:
586 > *
587 > * 1. **Direct command** (our format, without matcher):
588 > * ```yaml
589 > * hooks:
590 > * PreToolUse:
591 > * - type: command
592 > * command: "./scripts/validate.sh"
593 > * ```
594 > *
595 > * 2. **Nested with matcher** (Claude Code format):
596 > * ```yaml
597 > * hooks:
598 > * PreToolUse:
599 > * - matcher: "Bash"
600 > * hooks:
601 > * - type: command
602 > * command: "./scripts/validate.sh"
603 > * ```
604 > *
605 > * @param hooksMap The raw YAML map value from the `hooks` frontmatter attribute.
606 > * @param workspaceRootUri Workspace root for resolving relative `cwd` paths.
607 > * @param userHome User home directory path for tilde expansion.
608 > * @param target The agent's target, used to resolve hook type names correctly.
609 > * @returns Resolved hooks organized by hook type, ready for use in {@link ChatRequestHooks}.
610 > */
611 > export function parseSubagentHooksFromYaml(
612 hooksMap: IMapValue,
613 workspaceRootUri: URI | undefined,