diff --git a/build/next/index.ts b/build/next/index.ts index 2d30bbf737a51..2535364d64e6c 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -210,6 +210,10 @@ const commonResourcePatterns = [ // Tree-sitter queries 'vs/editor/common/languages/highlights/*.scm', 'vs/editor/common/languages/injections/*.scm', + + // SVGs referenced from CSS (needed for transpile/dev builds where CSS is copied as-is) + 'vs/workbench/browser/media/code-icon.svg', + 'vs/workbench/browser/parts/editor/media/letterpress*.svg', ]; // Resources only needed for dev/transpile builds (these get bundled into the main diff --git a/extensions/esbuild-extension-common.mts b/extensions/esbuild-extension-common.mts index b5ce1aceb0c78..da028ca7e02db 100644 --- a/extensions/esbuild-extension-common.mts +++ b/extensions/esbuild-extension-common.mts @@ -59,7 +59,7 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { options.format = 'cjs'; options.mainFields = ['module', 'main']; } else if (config.platform === 'browser') { - options.format = 'iife'; + options.format = 'cjs'; options.mainFields = ['browser', 'module', 'main']; options.alias = { 'path': 'path-browserify', diff --git a/package.json b/package.json index a43cf973e41c8..d5743f86c506d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "watch-client": "npm run gulp watch-client", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", - "watch-client-transpile": "npx tsx build/next/index.ts transpile --watch", + "watch-client-transpile": "node build/next/index.ts transpile --watch", "watch-client-transpiled": "deemon npm run watch-client-transpile", "kill-watch-client-transpiled": "deemon --kill npm run watch-client-transpile", "watch-extensions": "npm run gulp watch-extensions watch-extension-media", diff --git a/src/vs/workbench/api/browser/mainThreadPower.ts b/src/vs/workbench/api/browser/mainThreadPower.ts index dbd8078aa19c1..9f7ea9d7d5993 100644 --- a/src/vs/workbench/api/browser/mainThreadPower.ts +++ b/src/vs/workbench/api/browser/mainThreadPower.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostContext, ExtHostPowerShape, MainContext, MainThreadPowerShape, PowerSaveBlockerType, PowerSystemIdleState, PowerThermalState } from '../common/extHost.protocol.js'; import { IPowerService } from '../../services/power/common/powerService.js'; @@ -12,7 +12,6 @@ import { IPowerService } from '../../services/power/common/powerService.js'; export class MainThreadPower extends Disposable implements MainThreadPowerShape { private readonly proxy: ExtHostPowerShape; - private readonly disposables = this._register(new DisposableStore()); constructor( extHostContext: IExtHostContext, @@ -22,14 +21,14 @@ export class MainThreadPower extends Disposable implements MainThreadPowerShape this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostPower); // Forward power events to extension host - this.powerService.onDidSuspend(this.proxy.$onDidSuspend, this.proxy, this.disposables); - this.powerService.onDidResume(this.proxy.$onDidResume, this.proxy, this.disposables); - this.powerService.onDidChangeOnBatteryPower(this.proxy.$onDidChangeOnBatteryPower, this.proxy, this.disposables); - this.powerService.onDidChangeThermalState((state: PowerThermalState) => this.proxy.$onDidChangeThermalState(state), this, this.disposables); - this.powerService.onDidChangeSpeedLimit(this.proxy.$onDidChangeSpeedLimit, this.proxy, this.disposables); - this.powerService.onWillShutdown(this.proxy.$onWillShutdown, this.proxy, this.disposables); - this.powerService.onDidLockScreen(this.proxy.$onDidLockScreen, this.proxy, this.disposables); - this.powerService.onDidUnlockScreen(this.proxy.$onDidUnlockScreen, this.proxy, this.disposables); + this._register(this.powerService.onDidSuspend(this.proxy.$onDidSuspend, this.proxy)); + this._register(this.powerService.onDidResume(this.proxy.$onDidResume, this.proxy)); + this._register(this.powerService.onDidChangeOnBatteryPower(this.proxy.$onDidChangeOnBatteryPower, this.proxy)); + this._register(this.powerService.onDidChangeThermalState((state: PowerThermalState) => this.proxy.$onDidChangeThermalState(state), this)); + this._register(this.powerService.onDidChangeSpeedLimit(this.proxy.$onDidChangeSpeedLimit, this.proxy)); + this._register(this.powerService.onWillShutdown(this.proxy.$onWillShutdown, this.proxy)); + this._register(this.powerService.onDidLockScreen(this.proxy.$onDidLockScreen, this.proxy)); + this._register(this.powerService.onDidUnlockScreen(this.proxy.$onDidUnlockScreen, this.proxy)); } async $getSystemIdleState(idleThreshold: number): Promise { diff --git a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css index 19b64fc8c8522..201f1942042f4 100644 --- a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css +++ b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css @@ -7,9 +7,11 @@ .monaco-modal-editor-block { position: fixed; width: 100%; + height: 100%; + top: 0; left: 0; - /* z-index for modal editors: below dialogs, quick input, context views, hovers but above other things */ - z-index: 2000; + /* z-index for modal editors: above titlebar (2500) but below dialogs (2575) */ + z-index: 2550; display: flex; justify-content: center; align-items: center; diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 8d2c6acb5920e..9964e55f8407a 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -6,6 +6,7 @@ import './media/modalEditorPart.css'; import { $, addDisposableListener, append, EventHelper, EventType, isHTMLElement } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; @@ -28,6 +29,7 @@ import { IHostService } from '../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { localize } from '../../../../nls.js'; +import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js'; const defaultModalEditorAllowableCommands = new Set([ 'workbench.action.quit', @@ -36,6 +38,9 @@ const defaultModalEditorAllowableCommands = new Set([ 'workbench.action.closeAllEditors', 'workbench.action.files.save', 'workbench.action.files.saveAll', + CLOSE_MODAL_EDITOR_COMMAND_ID, + MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, + TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID ]); export interface ICreateModalEditorPartResult { @@ -60,7 +65,6 @@ export class ModalEditorPart { // Create modal container const modalElement = $('.monaco-modal-editor-block.dimmed'); - modalElement.tabIndex = -1; this.layoutService.mainContainer.appendChild(modalElement); disposables.add(toDisposable(() => modalElement.remove())); @@ -71,14 +75,15 @@ export class ModalEditorPart { const editorPartContainer = $('.part.editor.modal-editor-part', { role: 'dialog', 'aria-modal': 'true', - 'aria-labelledby': titleId + 'aria-labelledby': titleId, + tabIndex: -1 }); shadowElement.appendChild(editorPartContainer); // Create header with title and close button const headerElement = editorPartContainer.appendChild($('.modal-editor-header')); - // Title element (centered) + // Title element const titleElement = append(headerElement, $('div.modal-editor-title')); titleElement.id = titleId; titleElement.textContent = ''; @@ -102,7 +107,7 @@ export class ModalEditorPart { [IEditorService, modalEditorService] ))); - // Create toolbar driven by MenuId.ModalEditorTitle + // Create toolbar disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.ModalEditorTitle, { hiddenItemStrategy: HiddenItemStrategy.NoHide, menuOptions: { shouldForwardArgs: true } @@ -118,23 +123,43 @@ export class ModalEditorPart { editorPart.notifyActiveEditorChanged(); }))); - // Handle close on click outside (on the dimmed background) + // Handle double-click on header to toggle maximize + disposables.add(addDisposableListener(headerElement, EventType.DBLCLICK, e => { + EventHelper.stop(e); + + editorPart.toggleMaximized(); + })); + + // Guide focus back into the modal when clicking outside modal disposables.add(addDisposableListener(modalElement, EventType.MOUSE_DOWN, e => { if (e.target === modalElement) { - editorPart.close(); + EventHelper.stop(e, true); + + editorPartContainer.focus(); } })); // Block certain workbench commands from being dispatched while the modal is open disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); - const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer); - if (resolved.kind === ResultKind.KbFound && resolved.commandId) { - if ( - resolved.commandId.startsWith('workbench.') && - !defaultModalEditorAllowableCommands.has(resolved.commandId) - ) { - EventHelper.stop(event, true); + + // Close on Escape + if (event.equals(KeyCode.Escape)) { + EventHelper.stop(event, true); + + editorPart.close(); + } + + // Prevent unsupported commands + else { + const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer); + if (resolved.kind === ResultKind.KbFound && resolved.commandId) { + if ( + resolved.commandId.startsWith('workbench.') && + !defaultModalEditorAllowableCommands.has(resolved.commandId) + ) { + EventHelper.stop(event, true); + } } } })); @@ -168,10 +193,6 @@ export class ModalEditorPart { height = Math.min(height, availableHeight); // Ensure the modal never exceeds available height (below the title bar) - // Shift the modal block below the title bar - modalElement.style.top = `${titleBarOffset}px`; - modalElement.style.height = `calc(100% - ${titleBarOffset}px)`; - editorPartContainer.style.width = `${width}px`; editorPartContainer.style.height = `${height}px`; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts index fe17d4b9cd535..225803f5ae065 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCustomizationDiagnosticsAction.ts @@ -450,6 +450,8 @@ function getSkipReasonMessage(skipReason: PromptFileSkipReason | undefined, erro return nls.localize('status.typeDisabled', 'Disabled'); case 'all-hooks-disabled': return nls.localize('status.allHooksDisabled', 'All hooks disabled via disableAllHooks'); + case 'claude-hooks-disabled': + return nls.localize('status.claudeHooksDisabled', 'Claude hooks disabled via chat.useClaudeHooks setting'); default: return errorMessage ?? nls.localize('status.unknownError', 'Unknown error'); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts index 7f0aa2b785212..af3b2c394e33f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts @@ -114,7 +114,7 @@ class SkipToolConfirmation extends ToolConfirmationAction { } } -class ConfigureToolsAction extends Action2 { +export class ConfigureToolsAction extends Action2 { public static ID = 'workbench.action.chat.configureTools'; constructor() { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 035ee4ffe0801..b0d814cbf5785 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,9 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from '../../../../base/common/async.js'; import { Event } from '../../../../base/common/event.js'; -import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { isMacintosh } from '../../../../base/common/platform.js'; @@ -14,7 +12,6 @@ import { registerEditorFeature } from '../../../../editor/common/editorFeatures. import * as nls from '../../../../nls.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationNode, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; @@ -46,7 +43,7 @@ import { ChatTodoListService, IChatTodoListService } from '../common/tools/chatT import { ChatTransferService, IChatTransferService } from '../common/model/chatTransferService.js'; import { IChatVariablesService } from '../common/attachments/chatVariables.js'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from '../common/widget/chatWidgetHistoryService.js'; -import { AgentsControlClickBehavior, ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js'; +import { AgentsControlClickBehavior, ChatConfiguration } from '../common/constants.js'; import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } from '../common/ignoredFiles.js'; import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; @@ -66,13 +63,13 @@ import { BuiltinToolsContribution } from '../common/tools/builtinTools/tools.js' import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService.js'; import { registerChatAccessibilityActions } from './actions/chatAccessibilityActions.js'; import { AgentChatAccessibilityHelp, EditsChatAccessibilityHelp, PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js'; -import { ACTION_ID_NEW_CHAT, ModeOpenChatGlobalAction, registerChatActions } from './actions/chatActions.js'; +import { ModeOpenChatGlobalAction, registerChatActions } from './actions/chatActions.js'; import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; import { ChatContextContributions } from './actions/chatContext.js'; import { registerChatContextActions } from './actions/chatContextActions.js'; import { registerChatCopyActions } from './actions/chatCopyActions.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; -import { ChatSubmitAction, registerChatExecuteActions } from './actions/chatExecuteActions.js'; +import { registerChatExecuteActions } from './actions/chatExecuteActions.js'; import { registerChatFileTreeActions } from './actions/chatFileTreeActions.js'; import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js'; import { registerChatExportActions } from './actions/chatImportExport.js'; @@ -90,7 +87,7 @@ import { ChatTransferContribution } from './actions/chatTransfer.js'; import { registerChatCustomizationDiagnosticsAction } from './actions/chatCustomizationDiagnosticsAction.js'; import './agentSessions/agentSessions.contribution.js'; import { backgroundAgentDisplayName } from './agentSessions/agentSessions.js'; -import { IAgentSessionsService } from './agentSessions/agentSessionsService.js'; + import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from './chat.js'; import { ChatAccessibilityService } from './accessibility/chatAccessibilityService.js'; import './attachments/chatAttachmentModel.js'; @@ -111,7 +108,7 @@ import { ChatEditorInput, ChatEditorInputSerializer } from './widgetHosts/editor import { ChatLayoutService } from './widget/chatLayoutService.js'; import { ChatLanguageModelsDataContribution, LanguageModelsConfigurationService } from './languageModelsConfigurationService.js'; import './chatManagement/chatManagement.contribution.js'; -import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; + import { ChatOutputRendererService, IChatOutputRendererService } from './chatOutputItemRenderer.js'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from './chatParticipant.contribution.js'; import { ChatPasteProvidersFeature } from './widget/input/editor/chatPasteProviders.js'; @@ -132,7 +129,7 @@ import { LanguageModelToolsConfirmationService } from './tools/languageModelTool import { LanguageModelToolsService, globalAutoApproveDescription } from './tools/languageModelToolsService.js'; import './promptSyntax/promptCodingAgentActionContribution.js'; import './promptSyntax/promptToolsCodeLensProvider.js'; -import { showConfigureHooksQuickPick } from './promptSyntax/hookActions.js'; +import { ChatSlashCommandsContribution } from './chatSlashCommands.js'; import { PromptUrlHandler } from './promptSyntax/promptUrlHandler.js'; import { ConfigureToolSets, UserToolSetsContributions } from './tools/toolSetsContribution.js'; import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js'; @@ -1006,6 +1003,15 @@ configurationRegistry.registerConfiguration({ }, } }, + [PromptsConfig.USE_CLAUDE_HOOKS]: { + type: 'boolean', + title: nls.localize('chat.useClaudeHooks.title', "Use Claude Hooks",), + markdownDescription: nls.localize('chat.useClaudeHooks.description', "Controls whether hooks from Claude configuration files can execute. When disabled, only Copilot-format hooks are used. Hooks are loaded from the files configured in `#chat.hookFilesLocations#`.",), + default: false, + restricted: true, + disallowConfigurationDefault: true, + tags: ['preview', 'prompts', 'hooks', 'agent'] + }, [PromptsConfig.PROMPT_FILES_SUGGEST_KEY]: { type: 'object', scope: ConfigurationScope.RESOURCE, @@ -1398,150 +1404,11 @@ AccessibleViewRegistry.register(new EditsChatAccessibilityHelp()); AccessibleViewRegistry.register(new AgentChatAccessibilityHelp()); registerEditorFeature(ChatInputBoxContentProvider); - -class ChatSlashStaticSlashCommandsContribution extends Disposable { - - static readonly ID = 'workbench.contrib.chatSlashStaticSlashCommands'; - - constructor( - @IChatSlashCommandService slashCommandService: IChatSlashCommandService, - @ICommandService commandService: ICommandService, - @IChatAgentService chatAgentService: IChatAgentService, - @IChatWidgetService chatWidgetService: IChatWidgetService, - @IInstantiationService instantiationService: IInstantiationService, - @IAgentSessionsService agentSessionsService: IAgentSessionsService, - ) { - super(); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'clear', - detail: nls.localize('clear', "Start a new chat and archive the current one"), - sortText: 'z2_clear', - executeImmediately: true, - locations: [ChatAgentLocation.Chat] - }, async (_prompt, _progress, _history, _location, sessionResource) => { - agentSessionsService.getSession(sessionResource)?.setArchived(true); - commandService.executeCommand(ACTION_ID_NEW_CHAT); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'hooks', - detail: nls.localize('hooks', "Configure hooks"), - sortText: 'z3_hooks', - executeImmediately: true, - silent: true, - locations: [ChatAgentLocation.Chat] - }, async () => { - await instantiationService.invokeFunction(showConfigureHooksQuickPick); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'debug', - detail: nls.localize('debug', "Show Chat Debug View"), - sortText: 'z3_debug', - executeImmediately: true, - silent: true, - locations: [ChatAgentLocation.Chat] - }, async () => { - await commandService.executeCommand('github.copilot.debug.showChatLogView'); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'agents', - detail: nls.localize('agents', "Configure custom agents"), - sortText: 'z3_agents', - executeImmediately: true, - silent: true, - locations: [ChatAgentLocation.Chat] - }, async () => { - await commandService.executeCommand('workbench.action.chat.configure.customagents'); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'skills', - detail: nls.localize('skills', "Configure skills"), - sortText: 'z3_skills', - executeImmediately: true, - silent: true, - locations: [ChatAgentLocation.Chat] - }, async () => { - await commandService.executeCommand('workbench.action.chat.configure.skills'); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'instructions', - detail: nls.localize('instructions', "Configure instructions"), - sortText: 'z3_instructions', - executeImmediately: true, - silent: true, - locations: [ChatAgentLocation.Chat] - }, async () => { - await commandService.executeCommand('workbench.action.chat.configure.instructions'); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'prompts', - detail: nls.localize('prompts', "Configure prompt files"), - sortText: 'z3_prompts', - executeImmediately: true, - silent: true, - locations: [ChatAgentLocation.Chat] - }, async () => { - await commandService.executeCommand('workbench.action.chat.configure.prompts'); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'help', - detail: '', - sortText: 'z1_help', - executeImmediately: true, - locations: [ChatAgentLocation.Chat], - modes: [ChatModeKind.Ask] - }, async (prompt, progress, _history, _location, sessionResource) => { - const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat); - const agents = chatAgentService.getAgents(); - - // Report prefix - if (defaultAgent?.metadata.helpTextPrefix) { - if (isMarkdownString(defaultAgent.metadata.helpTextPrefix)) { - progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'markdownContent' }); - } else { - progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPrefix), kind: 'markdownContent' }); - } - progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); - } - - // Report agent list - const agentText = (await Promise.all(agents - .filter(a => !a.isDefault && !a.isCore) - .filter(a => a.locations.includes(ChatAgentLocation.Chat)) - .map(async a => { - const description = a.description ? `- ${a.description}` : ''; - const agentMarkdown = instantiationService.invokeFunction(accessor => agentToMarkdown(a, sessionResource, true, accessor)); - const agentLine = `- ${agentMarkdown} ${description}`; - const commandText = a.slashCommands.map(c => { - const description = c.description ? `- ${c.description}` : ''; - return `\t* ${agentSlashCommandToMarkdown(a, c, sessionResource)} ${description}`; - }).join('\n'); - - return (agentLine + '\n' + commandText).trim(); - }))).join('\n'); - progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [ChatSubmitAction.ID] } }), kind: 'markdownContent' }); - - // Report help text ending - if (defaultAgent?.metadata.helpTextPostfix) { - progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); - if (isMarkdownString(defaultAgent.metadata.helpTextPostfix)) { - progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'markdownContent' }); - } else { - progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPostfix), kind: 'markdownContent' }); - } - } - - // Without this, the response will be done before it renders and so it will not stream. This ensures that if the response starts - // rendering during the next 200ms, then it will be streamed. Once it starts streaming, the whole response streams even after - // it has received all response data has been received. - await timeout(200); - })); - } -} Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatLanguageModelsDataContribution.ID, ChatLanguageModelsDataContribution, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(ChatSlashStaticSlashCommandsContribution.ID, ChatSlashStaticSlashCommandsContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(ChatSlashCommandsContribution.ID, ChatSlashCommandsContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts new file mode 100644 index 0000000000000..34d97d4a40347 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { timeout } from '../../../../base/common/async.js'; +import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import * as nls from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IChatAgentService } from '../common/participants/chatAgents.js'; +import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; +import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; +import { ACTION_ID_NEW_CHAT } from './actions/chatActions.js'; +import { ChatSubmitAction, OpenModelPickerAction } from './actions/chatExecuteActions.js'; +import { ConfigureToolsAction } from './actions/chatToolActions.js'; +import { IAgentSessionsService } from './agentSessions/agentSessionsService.js'; +import { IChatWidgetService } from './chat.js'; +import { showConfigureHooksQuickPick } from './promptSyntax/hookActions.js'; +import { agentSlashCommandToMarkdown, agentToMarkdown } from './widget/chatContentParts/chatMarkdownDecorationsRenderer.js'; + +export class ChatSlashCommandsContribution extends Disposable { + + static readonly ID = 'workbench.contrib.chatSlashCommands'; + + constructor( + @IChatSlashCommandService slashCommandService: IChatSlashCommandService, + @ICommandService commandService: ICommandService, + @IChatAgentService chatAgentService: IChatAgentService, + @IChatWidgetService chatWidgetService: IChatWidgetService, + @IInstantiationService instantiationService: IInstantiationService, + @IAgentSessionsService agentSessionsService: IAgentSessionsService, + ) { + super(); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'clear', + detail: nls.localize('clear', "Start a new chat and archive the current one"), + sortText: 'z2_clear', + executeImmediately: true, + locations: [ChatAgentLocation.Chat] + }, async (_prompt, _progress, _history, _location, sessionResource) => { + agentSessionsService.getSession(sessionResource)?.setArchived(true); + commandService.executeCommand(ACTION_ID_NEW_CHAT); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'hooks', + detail: nls.localize('hooks', "Configure hooks"), + sortText: 'z3_hooks', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await instantiationService.invokeFunction(showConfigureHooksQuickPick); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'models', + detail: nls.localize('models', "Open the model picker"), + sortText: 'z3_models', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand(OpenModelPickerAction.ID); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'tools', + detail: nls.localize('tools', "Configure tools"), + sortText: 'z3_tools', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand(ConfigureToolsAction.ID); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'debug', + detail: nls.localize('debug', "Show Chat Debug View"), + sortText: 'z3_debug', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand('github.copilot.debug.showChatLogView'); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'agents', + detail: nls.localize('agents', "Configure custom agents"), + sortText: 'z3_agents', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand('workbench.action.chat.configure.customagents'); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'skills', + detail: nls.localize('skills', "Configure skills"), + sortText: 'z3_skills', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand('workbench.action.chat.configure.skills'); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'instructions', + detail: nls.localize('instructions', "Configure instructions"), + sortText: 'z3_instructions', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand('workbench.action.chat.configure.instructions'); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'prompts', + detail: nls.localize('prompts', "Configure prompt files"), + sortText: 'z3_prompts', + executeImmediately: true, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async () => { + await commandService.executeCommand('workbench.action.chat.configure.prompts'); + })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'help', + detail: '', + sortText: 'z1_help', + executeImmediately: true, + locations: [ChatAgentLocation.Chat], + modes: [ChatModeKind.Ask] + }, async (prompt, progress, _history, _location, sessionResource) => { + const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat); + const agents = chatAgentService.getAgents(); + + // Report prefix + if (defaultAgent?.metadata.helpTextPrefix) { + if (isMarkdownString(defaultAgent.metadata.helpTextPrefix)) { + progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'markdownContent' }); + } else { + progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPrefix), kind: 'markdownContent' }); + } + progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); + } + + // Report agent list + const agentText = (await Promise.all(agents + .filter(a => !a.isDefault && !a.isCore) + .filter(a => a.locations.includes(ChatAgentLocation.Chat)) + .map(async a => { + const description = a.description ? `- ${a.description}` : ''; + const agentMarkdown = instantiationService.invokeFunction(accessor => agentToMarkdown(a, sessionResource, true, accessor)); + const agentLine = `- ${agentMarkdown} ${description}`; + const commandText = a.slashCommands.map(c => { + const description = c.description ? `- ${c.description}` : ''; + return `\t* ${agentSlashCommandToMarkdown(a, c, sessionResource)} ${description}`; + }).join('\n'); + + return (agentLine + '\n' + commandText).trim(); + }))).join('\n'); + progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [ChatSubmitAction.ID] } }), kind: 'markdownContent' }); + + // Report help text ending + if (defaultAgent?.metadata.helpTextPostfix) { + progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); + if (isMarkdownString(defaultAgent.metadata.helpTextPostfix)) { + progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'markdownContent' }); + } else { + progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPostfix), kind: 'markdownContent' }); + } + } + + // Without this, the response will be done before it renders and so it will not stream. This ensures that if the response starts + // rendering during the next 200ms, then it will be streamed. Once it starts streaming, the whole response streams even after + // it has received all response data has been received. + await timeout(200); + })); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatDisabledClaudeHooksContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatDisabledClaudeHooksContentPart.ts new file mode 100644 index 0000000000000..7939aaa309915 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatDisabledClaudeHooksContentPart.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { createMarkdownCommandLink, MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { IMarkdownRendererService, openLinkFromMarkdown } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { localize } from '../../../../../../nls.js'; +import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; +import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; +import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; +import './media/chatDisabledClaudeHooksContent.css'; + +export class ChatDisabledClaudeHooksContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + constructor( + _context: IChatContentPartRenderContext, + @IOpenerService private readonly _openerService: IOpenerService, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, + ) { + super(); + + this.domNode = dom.$('.chat-disabled-claude-hooks'); + const messageContainer = dom.$('.chat-disabled-claude-hooks-message'); + + const icon = dom.$('.chat-disabled-claude-hooks-icon'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); + + const enableLink = createMarkdownCommandLink({ + title: localize('chat.disabledClaudeHooks.enableLink', "Enable"), + id: 'workbench.action.openSettings', + arguments: [PromptsConfig.USE_CLAUDE_HOOKS], + }); + const message = localize('chat.disabledClaudeHooks.message', "Claude Code hooks are available for this workspace. {0}", enableLink); + const content = new MarkdownString(message, { isTrusted: true }); + + const rendered = this._register(this._markdownRendererService.render(content, { + actionHandler: (href) => openLinkFromMarkdown(this._openerService, href, true), + })); + + messageContainer.appendChild(icon); + messageContainer.appendChild(rendered.element); + this.domNode.appendChild(messageContainer); + } + + hasSameContent(other: IChatRendererContent): boolean { + return other.kind === 'disabledClaudeHooks'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatDisabledClaudeHooksContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatDisabledClaudeHooksContent.css new file mode 100644 index 0000000000000..5874eda712ca9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatDisabledClaudeHooksContent.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-disabled-claude-hooks-message { + display: flex; + align-items: center; + gap: 8px; + font-style: italic; + font-size: 12px; + color: var(--vscode-descriptionForeground); + opacity: 0.8; +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index dd074702d2bec..df048af089739 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -58,7 +58,7 @@ import { IChatAgentMetadata } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatTextEditGroup } from '../../common/model/chatModel.js'; import { chatSubcommandLeader } from '../../common/requestParser/chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, ChatRequestQueueKind, IChatConfirmation, IChatContentReference, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatPullRequestContent, IChatQuestionCarousel, IChatService, IChatTask, IChatTaskSerialized, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, isChatFollowup } from '../../common/chatService/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, ChatRequestQueueKind, IChatConfirmation, IChatContentReference, IChatDisabledClaudeHooksPart, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatPullRequestContent, IChatQuestionCarousel, IChatService, IChatTask, IChatTaskSerialized, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, isChatFollowup } from '../../common/chatService/chatService.js'; import { localChatSessionType } from '../../common/chatSessionsService.js'; import { getChatSessionType } from '../../common/model/chatUri.js'; import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js'; @@ -86,6 +86,7 @@ import { ChatQuestionCarouselPart } from './chatContentParts/chatQuestionCarouse import { ChatExtensionsContentPart } from './chatContentParts/chatExtensionsContentPart.js'; import { ChatMarkdownContentPart, codeblockHasClosingBackticks } from './chatContentParts/chatMarkdownContentPart.js'; import { ChatMcpServersInteractionContentPart } from './chatContentParts/chatMcpServersInteractionContentPart.js'; +import { ChatDisabledClaudeHooksContentPart } from './chatContentParts/chatDisabledClaudeHooksContentPart.js'; import { ChatMultiDiffContentPart } from './chatContentParts/chatMultiDiffContentPart.js'; import { ChatProgressContentPart, ChatWorkingProgressContentPart } from './chatContentParts/chatProgressContentPart.js'; import { ChatPullRequestContentPart } from './chatContentParts/chatPullRequestContentPart.js'; @@ -1001,6 +1002,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && !IChatToolInvocation.isComplete(part))) || (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) || lastPart.kind === 'mcpServersStarting' || + lastPart.kind === 'disabledClaudeHooks' || lastPart.kind === 'hook' ) { return true; @@ -1742,6 +1744,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer; const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop']); @@ -502,6 +504,7 @@ class AbstractResponse implements IResponse { case 'multiDiffData': case 'mcpServersStarting': case 'questionCarousel': + case 'disabledClaudeHooks': // Ignore continue; case 'toolInvocation': @@ -1421,7 +1424,7 @@ interface ISerializableChatResponseData { timeSpentWaiting?: number; } -export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized | IChatQuestionCarousel; +export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized | IChatQuestionCarousel | IChatDisabledClaudeHooksPart; export interface ISerializableChatRequestData extends ISerializableChatResponseData { requestId: string; diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index b1b876d8f1f41..767cb087be6a4 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -81,6 +81,7 @@ const responsePartSchema = Adapt.v; + getHooks(token: CancellationToken): Promise; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 25ba3b58bd7fa..2ab2855d70430 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -32,11 +32,11 @@ import { AGENT_MD_FILENAME, CLAUDE_CONFIG_FOLDER, CLAUDE_LOCAL_MD_FILENAME, CLAU import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js'; -import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger, Target } from './promptsService.js'; +import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, IConfiguredHooksInfo, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger, Target } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; import { IChatRequestHooks, IHookCommand, HookType } from '../hookSchema.js'; -import { parseHooksFromFile } from '../hookCompatibility.js'; +import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../hookCompatibility.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IPathService } from '../../../../../services/path/common/pathService.js'; import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptValidator.js'; @@ -96,7 +96,7 @@ export class PromptsService extends Disposable implements IPromptsService { /** * Cached hooks. Invalidated when hook files change. */ - private readonly cachedHooks: CachedPromise; + private readonly cachedHooks: CachedPromise; /** * Cached skills. Caching only happens if the `onDidChangeSkills` event is used. @@ -179,7 +179,7 @@ export class PromptsService extends Disposable implements IPromptsService { (token) => this.computeHooks(token), () => Event.any( this.getFileLocatorEvent(PromptsType.hook), - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CHAT_HOOKS)), + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CHAT_HOOKS) || e.affectsConfiguration(PromptsConfig.USE_CLAUDE_HOOKS)), ) )); @@ -1002,16 +1002,17 @@ export class PromptsService extends Disposable implements IPromptsService { return result; } - public getHooks(token: CancellationToken): Promise { + public async getHooks(token: CancellationToken): Promise { return this.cachedHooks.get(token); } - private async computeHooks(token: CancellationToken): Promise { + private async computeHooks(token: CancellationToken): Promise { const useChatHooks = this.configurationService.getValue(PromptsConfig.USE_CHAT_HOOKS); if (!useChatHooks) { return undefined; } + const useClaudeHooks = this.configurationService.getValue(PromptsConfig.USE_CLAUDE_HOOKS); const hookFiles = await this.listPromptFiles(PromptsType.hook, token); if (hookFiles.length === 0) { @@ -1029,6 +1030,7 @@ export class PromptsService extends Disposable implements IPromptsService { const workspaceFolder = this.workspaceService.getWorkspace().folders[0]; const workspaceRootUri = workspaceFolder?.uri; + let hasDisabledClaudeHooks = false; const collectedHooks: Record = { [HookType.SessionStart]: [], [HookType.UserPromptSubmit]: [], @@ -1054,6 +1056,16 @@ export class PromptsService extends Disposable implements IPromptsService { continue; } + if (format === HookSourceFormat.Claude && useClaudeHooks === false) { + const hasAnyCommands = [...hooks.values()].some(({ hooks: cmds }) => cmds.length > 0); + if (hasAnyCommands) { + hasDisabledClaudeHooks = true; + } + + this.logger.trace(`[PromptsService] Skipping Claude hook file (disabled via setting): ${hookFile.uri}`); + continue; + } + for (const [hookType, { hooks: commands }] of hooks) { for (const command of commands) { collectedHooks[hookType].push(command); @@ -1078,7 +1090,7 @@ export class PromptsService extends Disposable implements IPromptsService { ) as IChatRequestHooks; this.logger.trace(`[PromptsService] Collected hooks: ${JSON.stringify(Object.keys(result))}`); - return result; + return { hooks: result, hasDisabledClaudeHooks }; } public async getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken): Promise { @@ -1331,6 +1343,7 @@ export class PromptsService extends Disposable implements IPromptsService { const workspaceFolder = this.workspaceService.getWorkspace().folders[0]; const workspaceRootUri = workspaceFolder?.uri; + const useClaudeHooks = this.configurationService.getValue(PromptsConfig.USE_CLAUDE_HOOKS); const hookFiles = await this.listPromptFiles(PromptsType.hook, token); for (const promptPath of hookFiles) { const uri = promptPath.uri; @@ -1338,6 +1351,19 @@ export class PromptsService extends Disposable implements IPromptsService { const extensionId = promptPath.extension?.identifier?.value; const name = basename(uri); + // Skip Claude hooks when the setting is disabled + if (getHookSourceFormat(uri) === HookSourceFormat.Claude && useClaudeHooks === false) { + files.push({ + uri, + storage, + status: 'skipped', + skipReason: 'claude-hooks-disabled', + name, + extensionId + }); + continue; + } + try { // Try to parse the JSON to validate it (supports JSONC with comments) const content = await this.fileService.readFile(uri); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index d8784ef6dd740..4e9eacf9ae365 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -250,7 +250,8 @@ export class RunSubagentTool extends Disposable implements IToolImpl { // Collect hooks from hook .json files let collectedHooks: IChatRequestHooks | undefined; try { - collectedHooks = await this.promptsService.getHooks(token); + const info = await this.promptsService.getHooks(token); + collectedHooks = info?.hooks; } catch (error) { this.logService.warn('[ChatService] Failed to collect hooks:', error); }