Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .github/CODENOTIFY
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ src/vs/code/** @bpasero @deepak1556
src/vs/workbench/services/activity/** @bpasero
src/vs/workbench/services/authentication/** @TylerLeonhardt
src/vs/workbench/services/auxiliaryWindow/** @bpasero
src/vs/workbench/services/chat/** @bpasero
src/vs/workbench/services/contextmenu/** @bpasero
src/vs/workbench/services/dialogs/** @alexr00 @bpasero
src/vs/workbench/services/editor/** @bpasero
Expand Down Expand Up @@ -100,15 +99,6 @@ src/vs/workbench/electron-browser/** @bpasero
src/vs/workbench/contrib/authentication/** @TylerLeonhardt
src/vs/workbench/contrib/files/** @bpasero
src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @roblourens
src/vs/workbench/contrib/chat/browser/chatSetup/** @bpasero
src/vs/workbench/contrib/chat/browser/chatStatus/** @bpasero
src/vs/workbench/contrib/chat/browser/chatViewPane.ts @bpasero
src/vs/workbench/contrib/chat/browser/media/chatViewPane.css @bpasero
src/vs/workbench/contrib/chat/browser/chatViewTitleControl.ts @bpasero
src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css @bpasero
src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts @bpasero
src/vs/workbench/contrib/chat/browser/chatManagement/media/chatUsageWidget.css @bpasero
src/vs/workbench/contrib/chat/browser/agentSessions/** @bpasero
src/vs/workbench/contrib/localization/** @TylerLeonhardt
src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @TylerLeonhardt
src/vs/workbench/contrib/scm/** @lszomoru
Expand Down
4 changes: 0 additions & 4 deletions build/azure-pipelines/product-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,6 @@ variables:
name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.VSCODE_QUALITY }})"

resources:
pipelines:
- pipeline: vscode-7pm-kick-off
source: 'VS Code 7PM Kick-Off'
trigger: true
repositories:
- repository: 1esPipelines
type: git
Expand Down
3 changes: 3 additions & 0 deletions extensions/json/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
".ember-cli",
"typedoc.json"
],
"filenamePatterns": [
"**/.github/hooks/*.json"
],
"configuration": "./language-configuration.json"
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,13 @@ async function collectHooksStatus(
const discoveryInfo = await promptsService.getPromptDiscoveryInfo(type, token);
const files = discoveryInfo.files.map(convertDiscoveryResultToFileStatus);

// Collect URIs of files skipped due to disableAllHooks so we can show their hidden hooks
const disabledFileUris = discoveryInfo.files
.filter(f => f.status === 'skipped' && f.skipReason === 'all-hooks-disabled')
.map(f => f.uri);

// Parse hook files to extract individual hooks grouped by lifecycle
const parsedHooks = await parseHookFiles(promptsService, fileService, labelService, pathService, workspaceContextService, remoteAgentService, token);
const parsedHooks = await parseHookFiles(promptsService, fileService, labelService, pathService, workspaceContextService, remoteAgentService, token, disabledFileUris);

return { type, paths, files, enabled, parsedHooks };
}
Expand All @@ -341,7 +346,8 @@ async function parseHookFiles(
pathService: IPathService,
workspaceContextService: IWorkspaceContextService,
remoteAgentService: IRemoteAgentService,
token: CancellationToken
token: CancellationToken,
additionalDisabledFileUris?: URI[]
): Promise<IParsedHook[]> {
// Get workspace root and user home for path resolution
const workspaceFolder = workspaceContextService.getWorkspace().folders[0];
Expand All @@ -354,7 +360,7 @@ async function parseHookFiles(
const targetOS = remoteEnv?.os ?? OS;

// Use the shared helper
return parseAllHookFiles(promptsService, fileService, labelService, workspaceRootUri, userHome, targetOS, token);
return parseAllHookFiles(promptsService, fileService, labelService, workspaceRootUri, userHome, targetOS, token, { additionalDisabledFileUris });
}

/**
Expand Down Expand Up @@ -442,6 +448,8 @@ function getSkipReasonMessage(skipReason: PromptFileSkipReason | undefined, erro
return errorMessage ?? nls.localize('status.parseError', 'Parse error');
case 'disabled':
return nls.localize('status.typeDisabled', 'Disabled');
case 'all-hooks-disabled':
return nls.localize('status.allHooksDisabled', 'All hooks disabled via disableAllHooks');
default:
return errorMessage ?? nls.localize('status.unknownError', 'Unknown error');
}
Expand Down Expand Up @@ -735,16 +743,22 @@ export function formatStatusOutput(
const fileHooks = hooksByFile.get(fileKey)!;
const firstHook = fileHooks[0];
const filePath = getRelativePath(firstHook.fileUri, workspaceFolders);
const fileDisabled = fileHooks[0].disabled;

// File as clickable link
lines.push(`[${firstHook.filePath}](${filePath})<br>`);
// File as clickable link, with note if hooks are disabled via flag
if (fileDisabled) {
lines.push(`[${firstHook.filePath}](${filePath}) - *${nls.localize('status.allHooksDisabledLabel', 'all hooks disabled via disableAllHooks')}*<br>`);
} else {
lines.push(`[${firstHook.filePath}](${filePath})<br>`);
}

// Flatten hooks with their lifecycle label
for (let i = 0; i < fileHooks.length; i++) {
const hook = fileHooks[i];
const isLast = i === fileHooks.length - 1;
const prefix = isLast ? TREE_END : TREE_BRANCH;
lines.push(`${prefix} ${hook.hookTypeLabel}: \`${hook.commandLabel}\`<br>`);
const disabledPrefix = hook.disabled ? `${ICON_ERROR} ` : '';
lines.push(`${prefix} ${disabledPrefix}${hook.hookTypeLabel}: \`${hook.commandLabel}\`<br>`);
}
}
hasContent = true;
Expand Down
55 changes: 51 additions & 4 deletions src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { findNodeAtLocation, Node, parseTree } from '../../../../../base/common/json.js';
import { findNodeAtLocation, Node, parse as parseJSONC, parseTree } from '../../../../../base/common/json.js';
import { ITextEditorSelection } from '../../../../../platform/editor/common/editor.js';
import { URI } from '../../../../../base/common/uri.js';
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { formatHookCommandLabel, HOOK_TYPES, HookType, IHookCommand } from '../../common/promptSyntax/hookSchema.js';
import { parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js';
import { parseHooksFromFile, parseHooksIgnoringDisableAll } from '../../common/promptSyntax/hookCompatibility.js';
import * as nls from '../../../../../nls.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { OperatingSystem } from '../../../../../base/common/platform.js';
Expand Down Expand Up @@ -126,6 +126,13 @@ export interface IParsedHook {
index: number;
/** The original hook type ID as it appears in the JSON file */
originalHookTypeId: string;
/** If true, this hook is disabled via `disableAllHooks: true` in its file */
disabled?: boolean;
}

export interface IParseAllHookFilesOptions {
/** Additional file URIs to parse (e.g., files skipped due to disableAllHooks) */
additionalDisabledFileUris?: readonly URI[];
}

/**
Expand All @@ -139,15 +146,16 @@ export async function parseAllHookFiles(
workspaceRootUri: URI | undefined,
userHome: string,
os: OperatingSystem,
token: CancellationToken
token: CancellationToken,
options?: IParseAllHookFilesOptions
): Promise<IParsedHook[]> {
const hookFiles = await promptsService.listPromptFiles(PromptsType.hook, token);
const parsedHooks: IParsedHook[] = [];

for (const hookFile of hookFiles) {
try {
const content = await fileService.readFile(hookFile.uri);
const json = JSON.parse(content.value.toString());
const json = parseJSONC(content.value.toString());

// Use format-aware parsing
const { hooks } = parseHooksFromFile(hookFile.uri, json, workspaceRootUri, userHome);
Expand Down Expand Up @@ -179,5 +187,44 @@ export async function parseAllHookFiles(
}
}

// Parse additional disabled files (e.g., files with disableAllHooks: true)
// These are parsed ignoring the disableAllHooks flag so we can show their hooks as disabled
if (options?.additionalDisabledFileUris) {
for (const uri of options.additionalDisabledFileUris) {
try {
const content = await fileService.readFile(uri);
const json = parseJSONC(content.value.toString());

// Parse hooks ignoring disableAllHooks - use the underlying format parsers directly
const { hooks } = parseHooksIgnoringDisableAll(uri, json, workspaceRootUri, userHome);

for (const [hookType, { hooks: commands, originalId }] of hooks) {
const hookTypeMeta = HOOK_TYPES.find(h => h.id === hookType);
if (!hookTypeMeta) {
continue;
}

for (let i = 0; i < commands.length; i++) {
const command = commands[i];
const commandLabel = formatHookCommandLabel(command, os) || nls.localize('commands.hook.emptyCommand', '(empty command)');
parsedHooks.push({
hookType,
hookTypeLabel: hookTypeMeta.label,
command,
commandLabel,
fileUri: uri,
filePath: labelService.getUriLabel(uri, { relative: true }),
index: i,
originalHookTypeId: originalId,
disabled: true
});
}
}
} catch (error) {
console.error('Failed to read or parse disabled hook file', uri.toString(), error);
}
}
}

return parsedHooks;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToo
import { chatSessionResourceToId } from '../../common/model/chatUri.js';
import { HookType } from '../../common/promptSyntax/hookSchema.js';
import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';
import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, IExternalPreToolUseHookResult, ILanguageModelToolsService, IPreparedToolInvocation, isToolSet, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolResultInputOutputDetails, IToolSet, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, toolMatchesModel, ToolSet, ToolSetForModel, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js';
import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, IExternalPreToolUseHookResult, ILanguageModelToolsService, IPreparedToolInvocation, isToolSet, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolResultInputOutputDetails, IToolSet, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolInvocationPresentation, toolMatchesModel, ToolSet, ToolSetForModel, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js';
import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js';

const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);
Expand Down Expand Up @@ -209,6 +209,13 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
if (agentModeEnabled !== false) {
return true;
}

// Internal tools that explicitly cannot be referenced in prompts are always permitted
// since they are infrastructure tools (e.g. inline_chat_exit), not user-facing agent tools
if (!isToolSet(toolOrToolSet) && toolOrToolSet.canBeReferencedInPrompt === false && toolOrToolSet.source.type === 'internal') {
return true;
}

const permittedInternalToolSetIds = [SpecedToolAliases.read, SpecedToolAliases.search, SpecedToolAliases.web];
if (isToolSet(toolOrToolSet)) {
const permitted = toolOrToolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolOrToolSet.referenceName);
Expand Down Expand Up @@ -377,6 +384,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo

if (toolData) {
if (pendingInvocation) {
pendingInvocation.presentation = ToolInvocationPresentation.Hidden;
pendingInvocation.cancelFromStreaming(ToolConfirmKind.Denied, reason);
} else if (request) {
const cancelledInvocation = ChatToolInvocation.createCancelled(
Expand All @@ -385,6 +393,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
ToolConfirmKind.Denied,
reason
);
cancelledInvocation.presentation = ToolInvocationPresentation.Hidden;
this._chatService.appendProgress(request, cancelledInvocation);
}
}
Expand Down Expand Up @@ -726,17 +735,49 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
}
const fullReferenceName = getToolFullReferenceName(tool.data);
const hookReason = hookResult.permissionDecisionReason;
const baseMessage = localize('hookRequiresConfirmation.message', "{0} hook confirmation required", HookType.PreToolUse);
const hookNote = hookReason
? localize('hookRequiresConfirmation.messageWithReason', "{0} hook required confirmation: {1}", HookType.PreToolUse, hookReason)
: localize('hookRequiresConfirmation.message', "{0} hook required confirmation", HookType.PreToolUse);
preparedInvocation.confirmationMessages = {
...preparedInvocation.confirmationMessages,
title: localize('hookRequiresConfirmation.title', "Use the '{0}' tool?", fullReferenceName),
message: new MarkdownString(hookReason ? `${baseMessage}\n\n${hookReason}` : baseMessage),
message: new MarkdownString(`_${hookNote}_`),
allowAutoConfirm: false,
};
preparedInvocation.toolSpecificData = {
kind: 'input',
rawInput: dto.parameters,
};
} else {
// Tool already has its own confirmation - prepend hook note
const hookReason = hookResult.permissionDecisionReason;
const hookNote = hookReason
? localize('hookRequiresConfirmation.note', "{0} hook required confirmation: {1}", HookType.PreToolUse, hookReason)
: localize('hookRequiresConfirmation.noteNoReason', "{0} hook required confirmation", HookType.PreToolUse);

const existing = preparedInvocation.confirmationMessages!;
if (preparedInvocation.toolSpecificData?.kind === 'terminal') {
// Terminal tools render message as hover only; use disclaimer for visible text
const existingDisclaimerText = existing.disclaimer
? (typeof existing.disclaimer === 'string' ? existing.disclaimer : existing.disclaimer.value)
: undefined;
const combinedDisclaimer = existingDisclaimerText
? `${hookNote}\n\n${existingDisclaimerText}`
: hookNote;
preparedInvocation.confirmationMessages = {
...existing,
disclaimer: combinedDisclaimer,
allowAutoConfirm: false,
};
} else {
// Edit/other tools: prepend hook note to the message body
const msgText = typeof existing.message === 'string' ? existing.message : existing.message?.value ?? '';
preparedInvocation.confirmationMessages = {
...existing,
message: new MarkdownString(`_${hookNote}_\n\n${msgText}`),
allowAutoConfirm: false,
};
}
}
return { autoConfirmed: undefined, preparedInvocation };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { localize } from '../../../../../../nls.js';
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
import { IChatHookPart } from '../../../common/chatService/chatService.js';
import { IChatRendererContent } from '../../../common/model/chatViewModel.js';
import { HOOK_TYPES, HookTypeValue } from '../../../common/promptSyntax/hookSchema.js';
import { HookType, HOOK_TYPES, HookTypeValue } from '../../../common/promptSyntax/hookSchema.js';
import { ChatTreeItem } from '../../chat.js';
import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
Expand All @@ -29,16 +29,23 @@ export class ChatHookContentPart extends ChatCollapsibleContentPart implements I
const hookTypeLabel = getHookTypeLabel(hookPart.hookType);
const isStopped = !!hookPart.stopReason;
const isWarning = !!hookPart.systemMessage;
const toolName = hookPart.toolDisplayName;
const title = isStopped
? localize('hook.title.stopped', "Blocked by {0} hook", hookTypeLabel)
: localize('hook.title.warning', "Warning from {0} hook", hookTypeLabel);
? (toolName
? localize('hook.title.stoppedWithTool', "Blocked {0} - {1} hook", toolName, hookTypeLabel)
: localize('hook.title.stopped', "Blocked by {0} hook", hookTypeLabel))
: (toolName
? localize('hook.title.warningWithTool', "Warning for {0} - {1} hook", toolName, hookTypeLabel)
: localize('hook.title.warning', "Warning from {0} hook", hookTypeLabel));

super(title, context, undefined, hoverService);

this.icon = isStopped ? Codicon.circleSlash : isWarning ? Codicon.warning : Codicon.check;
this.icon = isStopped ? Codicon.error : isWarning ? Codicon.warning : Codicon.check;

if (isStopped) {
this.domNode.classList.add('chat-hook-outcome-blocked');
} else if (isWarning) {
this.domNode.classList.add('chat-hook-outcome-warning');
}

this.setExpanded(false);
Expand All @@ -50,7 +57,10 @@ export class ChatHookContentPart extends ChatCollapsibleContentPart implements I
if (this.hookPart.stopReason) {
const reasonElement = $('.chat-hook-reason', undefined, this.hookPart.stopReason);
content.appendChild(reasonElement);
} else if (this.hookPart.systemMessage) {
}

const isToolHook = this.hookPart.hookType === HookType.PreToolUse || this.hookPart.hookType === HookType.PostToolUse;
if (this.hookPart.systemMessage && (isToolHook || !this.hookPart.stopReason)) {
const messageElement = $('.chat-hook-message', undefined, this.hookPart.systemMessage);
content.appendChild(messageElement);
}
Expand All @@ -64,6 +74,7 @@ export class ChatHookContentPart extends ChatCollapsibleContentPart implements I
}
return other.hookType === this.hookPart.hookType &&
other.stopReason === this.hookPart.stopReason &&
other.systemMessage === this.hookPart.systemMessage;
other.systemMessage === this.hookPart.systemMessage &&
other.toolDisplayName === this.hookPart.toolDisplayName;
}
}
Loading
Loading