From 700806ce839cb198cbf54db762203eaeeef3a613 Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Wed, 11 Mar 2026 15:09:49 +0800 Subject: [PATCH 1/3] fix: correct hooks JSON schema type definition The hooks array items were incorrectly typed as 'string' in the JSON schema, causing VS Code to show type errors when users configure HookDefinition objects. This fix adds proper schema support for complex array item types. - Add SettingItemDefinition interface for array item schema - Add items schema for UserPromptSubmit and Stop hooks - Update generate-settings-schema.ts to convert complex item types Fixes #2246 Co-authored-by: Qwen-Coder --- packages/cli/src/config/settingsSchema.ts | 145 ++++++++++++++++++ .../schemas/settings.schema.json | 114 +++++++++++++- scripts/generate-settings-schema.ts | 58 ++++++- 3 files changed, 314 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 4701abc1a..f9c043b3d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -76,6 +76,29 @@ export interface SettingDefinition { mergeStrategy?: MergeStrategy; /** Enum type options */ options?: readonly SettingEnumOption[]; + /** Schema for array items when type is 'array' */ + items?: SettingItemDefinition; +} + +/** + * Schema definition for array item types. + * Supports simple types (string, number, boolean) and complex object types. + */ +export interface SettingItemDefinition { + type: 'string' | 'number' | 'boolean' | 'object'; + properties?: Record< + string, + SettingItemDefinition & { + required?: boolean; + enum?: string[]; + additionalProperties?: SettingItemDefinition; + } + >; + items?: SettingItemDefinition; + required?: boolean; + enum?: string[]; + description?: string; + additionalProperties?: boolean | SettingItemDefinition; } export interface SettingsSchema { @@ -1233,6 +1256,67 @@ const SETTINGS_SCHEMA = { 'Hooks that execute before agent processing. Can modify prompts or inject context.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, + items: { + type: 'object', + description: + 'A hook definition with an optional matcher and a list of hook configurations.', + properties: { + matcher: { + type: 'string', + description: + 'An optional matcher pattern to filter when this hook definition applies.', + }, + sequential: { + type: 'boolean', + description: + 'Whether the hooks should be executed sequentially instead of in parallel.', + }, + hooks: { + type: 'object', + description: 'The list of hook configurations to execute.', + required: true, + items: { + type: 'object', + description: + 'A hook configuration entry that defines a command to execute.', + properties: { + type: { + type: 'string', + description: 'The type of hook.', + enum: ['command'], + required: true, + }, + command: { + type: 'string', + description: + 'The command to execute when the hook is triggered.', + required: true, + }, + name: { + type: 'string', + description: 'An optional name for the hook.', + }, + description: { + type: 'string', + description: + 'An optional description of what the hook does.', + }, + timeout: { + type: 'number', + description: + 'Timeout in milliseconds for the hook execution.', + }, + env: { + type: 'object', + description: + 'Environment variables to set when executing the hook command.', + additionalProperties: { type: 'string' }, + }, + }, + }, + }, + }, + }, }, Stop: { type: 'array', @@ -1244,6 +1328,67 @@ const SETTINGS_SCHEMA = { 'Hooks that execute after agent processing. Can post-process responses or log interactions.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, + items: { + type: 'object', + description: + 'A hook definition with an optional matcher and a list of hook configurations.', + properties: { + matcher: { + type: 'string', + description: + 'An optional matcher pattern to filter when this hook definition applies.', + }, + sequential: { + type: 'boolean', + description: + 'Whether the hooks should be executed sequentially instead of in parallel.', + }, + hooks: { + type: 'object', + description: 'The list of hook configurations to execute.', + required: true, + items: { + type: 'object', + description: + 'A hook configuration entry that defines a command to execute.', + properties: { + type: { + type: 'string', + description: 'The type of hook.', + enum: ['command'], + required: true, + }, + command: { + type: 'string', + description: + 'The command to execute when the hook is triggered.', + required: true, + }, + name: { + type: 'string', + description: 'An optional name for the hook.', + }, + description: { + type: 'string', + description: + 'An optional description of what the hook does.', + }, + timeout: { + type: 'number', + description: + 'Timeout in milliseconds for the hook execution.', + }, + env: { + type: 'object', + description: + 'Environment variables to set when executing the hook command.', + additionalProperties: { type: 'string' }, + }, + }, + }, + }, + }, + }, }, }, }, diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index d0eef6ae9..f063da94d 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -600,14 +600,124 @@ "description": "Hooks that execute before agent processing. Can modify prompts or inject context.", "type": "array", "items": { - "type": "string" + "description": "A hook definition with an optional matcher and a list of hook configurations.", + "type": "object", + "properties": { + "matcher": { + "description": "An optional matcher pattern to filter when this hook definition applies.", + "type": "string" + }, + "sequential": { + "description": "Whether the hooks should be executed sequentially instead of in parallel.", + "type": "boolean" + }, + "hooks": { + "description": "The list of hook configurations to execute.", + "type": "array", + "items": { + "description": "A hook configuration entry that defines a command to execute.", + "type": "object", + "properties": { + "type": { + "description": "The type of hook.", + "type": "string", + "enum": [ + "command" + ] + }, + "command": { + "description": "The command to execute when the hook is triggered.", + "type": "string" + }, + "name": { + "description": "An optional name for the hook.", + "type": "string" + }, + "description": { + "description": "An optional description of what the hook does.", + "type": "string" + }, + "timeout": { + "description": "Timeout in milliseconds for the hook execution.", + "type": "number" + }, + "env": { + "description": "Environment variables to set when executing the hook command.", + "type": "object" + } + }, + "required": [ + "type", + "command" + ] + } + } + }, + "required": [ + "hooks" + ] } }, "Stop": { "description": "Hooks that execute after agent processing. Can post-process responses or log interactions.", "type": "array", "items": { - "type": "string" + "description": "A hook definition with an optional matcher and a list of hook configurations.", + "type": "object", + "properties": { + "matcher": { + "description": "An optional matcher pattern to filter when this hook definition applies.", + "type": "string" + }, + "sequential": { + "description": "Whether the hooks should be executed sequentially instead of in parallel.", + "type": "boolean" + }, + "hooks": { + "description": "The list of hook configurations to execute.", + "type": "array", + "items": { + "description": "A hook configuration entry that defines a command to execute.", + "type": "object", + "properties": { + "type": { + "description": "The type of hook.", + "type": "string", + "enum": [ + "command" + ] + }, + "command": { + "description": "The command to execute when the hook is triggered.", + "type": "string" + }, + "name": { + "description": "An optional name for the hook.", + "type": "string" + }, + "description": { + "description": "An optional description of what the hook does.", + "type": "string" + }, + "timeout": { + "description": "Timeout in milliseconds for the hook execution.", + "type": "number" + }, + "env": { + "description": "Environment variables to set when executing the hook command.", + "type": "object" + } + }, + "required": [ + "type", + "command" + ] + } + } + }, + "required": [ + "hooks" + ] } } } diff --git a/scripts/generate-settings-schema.ts b/scripts/generate-settings-schema.ts index 9d13e8166..272d722d1 100644 --- a/scripts/generate-settings-schema.ts +++ b/scripts/generate-settings-schema.ts @@ -21,6 +21,7 @@ import { fileURLToPath } from 'node:url'; import type { SettingDefinition, + SettingItemDefinition, SettingsSchema, } from '../packages/cli/src/config/settingsSchema.js'; import { getSettingsSchema } from '../packages/cli/src/config/settingsSchema.js'; @@ -37,6 +38,57 @@ interface JsonSchemaProperty { enum?: (string | number)[]; default?: unknown; additionalProperties?: boolean | JsonSchemaProperty; + required?: string[]; +} + +function convertItemDefinitionToJsonSchema( + itemDef: SettingItemDefinition, +): JsonSchemaProperty { + const schema: JsonSchemaProperty = {}; + + if (itemDef.description) { + schema.description = itemDef.description; + } + + schema.type = itemDef.type; + + if (itemDef.enum) { + schema.enum = itemDef.enum; + } + + if (itemDef.type === 'object' && itemDef.properties) { + schema.properties = {}; + const requiredFields: string[] = []; + + for (const [key, childDef] of Object.entries(itemDef.properties)) { + const childSchema = convertItemDefinitionToJsonSchema(childDef); + schema.properties[key] = childSchema; + if (childDef.required) { + requiredFields.push(key); + } + } + + if (requiredFields.length > 0) { + schema.required = requiredFields; + } + + if (itemDef.additionalProperties !== undefined) { + if (typeof itemDef.additionalProperties === 'boolean') { + schema.additionalProperties = itemDef.additionalProperties; + } else { + schema.additionalProperties = convertItemDefinitionToJsonSchema( + itemDef.additionalProperties, + ); + } + } + } + + if (itemDef.items) { + schema.type = 'array'; + schema.items = convertItemDefinitionToJsonSchema(itemDef.items); + } + + return schema; } function convertSettingToJsonSchema( @@ -60,7 +112,11 @@ function convertSettingToJsonSchema( break; case 'array': schema.type = 'array'; - schema.items = { type: 'string' }; + if (setting.items) { + schema.items = convertItemDefinitionToJsonSchema(setting.items); + } else { + schema.items = { type: 'string' }; + } break; case 'enum': if (setting.options && setting.options.length > 0) { From d5eda197c2f31341275f56796a44608d068e8e6e Mon Sep 17 00:00:00 2001 From: xwj02155382 Date: Wed, 11 Mar 2026 15:23:01 +0800 Subject: [PATCH 2/3] refactor: extract HOOK_DEFINITION_ITEMS constant Extract common hook definition items schema into a reusable constant to avoid code duplication between UserPromptSubmit and Stop hooks. Co-authored-by: Qwen-Coder --- packages/cli/src/config/settingsSchema.ts | 187 ++++++++-------------- 1 file changed, 65 insertions(+), 122 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index f9c043b3d..c8c69ec6a 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -105,6 +105,69 @@ export interface SettingsSchema { [key: string]: SettingDefinition; } +/** + * Common items schema for hook definitions. + * Used by both UserPromptSubmit and Stop hooks. + */ +const HOOK_DEFINITION_ITEMS: SettingItemDefinition = { + type: 'object', + description: + 'A hook definition with an optional matcher and a list of hook configurations.', + properties: { + matcher: { + type: 'string', + description: + 'An optional matcher pattern to filter when this hook definition applies.', + }, + sequential: { + type: 'boolean', + description: + 'Whether the hooks should be executed sequentially instead of in parallel.', + }, + hooks: { + type: 'object', + description: 'The list of hook configurations to execute.', + required: true, + items: { + type: 'object', + description: + 'A hook configuration entry that defines a command to execute.', + properties: { + type: { + type: 'string', + description: 'The type of hook.', + enum: ['command'], + required: true, + }, + command: { + type: 'string', + description: 'The command to execute when the hook is triggered.', + required: true, + }, + name: { + type: 'string', + description: 'An optional name for the hook.', + }, + description: { + type: 'string', + description: 'An optional description of what the hook does.', + }, + timeout: { + type: 'number', + description: 'Timeout in milliseconds for the hook execution.', + }, + env: { + type: 'object', + description: + 'Environment variables to set when executing the hook command.', + additionalProperties: { type: 'string' }, + }, + }, + }, + }, + }, +}; + export type MemoryImportFormat = 'tree' | 'flat'; export type DnsResolutionOrder = 'ipv4first' | 'verbatim'; @@ -1256,67 +1319,7 @@ const SETTINGS_SCHEMA = { 'Hooks that execute before agent processing. Can modify prompts or inject context.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, - items: { - type: 'object', - description: - 'A hook definition with an optional matcher and a list of hook configurations.', - properties: { - matcher: { - type: 'string', - description: - 'An optional matcher pattern to filter when this hook definition applies.', - }, - sequential: { - type: 'boolean', - description: - 'Whether the hooks should be executed sequentially instead of in parallel.', - }, - hooks: { - type: 'object', - description: 'The list of hook configurations to execute.', - required: true, - items: { - type: 'object', - description: - 'A hook configuration entry that defines a command to execute.', - properties: { - type: { - type: 'string', - description: 'The type of hook.', - enum: ['command'], - required: true, - }, - command: { - type: 'string', - description: - 'The command to execute when the hook is triggered.', - required: true, - }, - name: { - type: 'string', - description: 'An optional name for the hook.', - }, - description: { - type: 'string', - description: - 'An optional description of what the hook does.', - }, - timeout: { - type: 'number', - description: - 'Timeout in milliseconds for the hook execution.', - }, - env: { - type: 'object', - description: - 'Environment variables to set when executing the hook command.', - additionalProperties: { type: 'string' }, - }, - }, - }, - }, - }, - }, + items: HOOK_DEFINITION_ITEMS, }, Stop: { type: 'array', @@ -1328,67 +1331,7 @@ const SETTINGS_SCHEMA = { 'Hooks that execute after agent processing. Can post-process responses or log interactions.', showInDialog: false, mergeStrategy: MergeStrategy.CONCAT, - items: { - type: 'object', - description: - 'A hook definition with an optional matcher and a list of hook configurations.', - properties: { - matcher: { - type: 'string', - description: - 'An optional matcher pattern to filter when this hook definition applies.', - }, - sequential: { - type: 'boolean', - description: - 'Whether the hooks should be executed sequentially instead of in parallel.', - }, - hooks: { - type: 'object', - description: 'The list of hook configurations to execute.', - required: true, - items: { - type: 'object', - description: - 'A hook configuration entry that defines a command to execute.', - properties: { - type: { - type: 'string', - description: 'The type of hook.', - enum: ['command'], - required: true, - }, - command: { - type: 'string', - description: - 'The command to execute when the hook is triggered.', - required: true, - }, - name: { - type: 'string', - description: 'An optional name for the hook.', - }, - description: { - type: 'string', - description: - 'An optional description of what the hook does.', - }, - timeout: { - type: 'number', - description: - 'Timeout in milliseconds for the hook execution.', - }, - env: { - type: 'object', - description: - 'Environment variables to set when executing the hook command.', - additionalProperties: { type: 'string' }, - }, - }, - }, - }, - }, - }, + items: HOOK_DEFINITION_ITEMS, }, }, }, From 8161ac45236f46416677c75444c0901362e72414 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Sun, 15 Mar 2026 20:32:56 +0800 Subject: [PATCH 3/3] fix(hooks): correct JSON schema type for hooks configuration - Add 'array' type support to SettingItemDefinition - Change hooks field from object to array type - Add additionalProperties constraint for env fields - Fix additionalProperties generation to only apply for object types This ensures the hooks configuration schema correctly represents hooks as an array and properly validates environment variable objects. Co-authored-by: Qwen-Coder --- packages/cli/src/config/settingsSchema.ts | 4 ++-- .../schemas/settings.schema.json | 10 ++++++++-- scripts/generate-settings-schema.ts | 16 ++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c8c69ec6a..6e6782f47 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -85,7 +85,7 @@ export interface SettingDefinition { * Supports simple types (string, number, boolean) and complex object types. */ export interface SettingItemDefinition { - type: 'string' | 'number' | 'boolean' | 'object'; + type: 'string' | 'number' | 'boolean' | 'object' | 'array'; properties?: Record< string, SettingItemDefinition & { @@ -125,7 +125,7 @@ const HOOK_DEFINITION_ITEMS: SettingItemDefinition = { 'Whether the hooks should be executed sequentially instead of in parallel.', }, hooks: { - type: 'object', + type: 'array', description: 'The list of hook configurations to execute.', required: true, items: { diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index f063da94d..bbd2df6b7 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -643,7 +643,10 @@ }, "env": { "description": "Environment variables to set when executing the hook command.", - "type": "object" + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "required": [ @@ -705,7 +708,10 @@ }, "env": { "description": "Environment variables to set when executing the hook command.", - "type": "object" + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "required": [ diff --git a/scripts/generate-settings-schema.ts b/scripts/generate-settings-schema.ts index 272d722d1..903131219 100644 --- a/scripts/generate-settings-schema.ts +++ b/scripts/generate-settings-schema.ts @@ -71,15 +71,15 @@ function convertItemDefinitionToJsonSchema( if (requiredFields.length > 0) { schema.required = requiredFields; } + } - if (itemDef.additionalProperties !== undefined) { - if (typeof itemDef.additionalProperties === 'boolean') { - schema.additionalProperties = itemDef.additionalProperties; - } else { - schema.additionalProperties = convertItemDefinitionToJsonSchema( - itemDef.additionalProperties, - ); - } + if (itemDef.type === 'object' && itemDef.additionalProperties !== undefined) { + if (typeof itemDef.additionalProperties === 'boolean') { + schema.additionalProperties = itemDef.additionalProperties; + } else { + schema.additionalProperties = convertItemDefinitionToJsonSchema( + itemDef.additionalProperties, + ); } }