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
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const _allApiProposals = {
},
chatHooks: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatHooks.d.ts',
version: 1
version: 2
},
chatOutputRenderer: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatOutputRenderer.d.ts',
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/api/browser/mainThreadHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { URI, UriComponents } from '../../../base/common/uri.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
import { ExtHostContext, MainContext, MainThreadHooksShape } from '../common/extHost.protocol.js';
import { HookResultKind, IHookResult, IHooksExecutionProxy, IHooksExecutionService } from '../../contrib/chat/common/hooksExecutionService.js';
import { HookCommandResultKind, IHookCommandResult, IHookResult, IHooksExecutionProxy, IHooksExecutionService } from '../../contrib/chat/common/hooksExecutionService.js';
import { HookTypeValue, IHookCommand } from '../../contrib/chat/common/promptSyntax/hookSchema.js';
import { CancellationToken } from '../../../base/common/cancellation.js';

Expand All @@ -22,10 +22,10 @@ export class MainThreadHooks extends Disposable implements MainThreadHooksShape
const extHostProxy = extHostContext.getProxy(ExtHostContext.ExtHostHooks);

const proxy: IHooksExecutionProxy = {
runHookCommand: async (hookCommand: IHookCommand, input: unknown, token: CancellationToken): Promise<IHookResult> => {
runHookCommand: async (hookCommand: IHookCommand, input: unknown, token: CancellationToken): Promise<IHookCommandResult> => {
const result = await extHostProxy.$runHookCommand(hookCommand, input, token);
return {
kind: result.kind as HookResultKind,
kind: result.kind as HookCommandResultKind,
result: result.result
};
}
Expand Down
1 change: 0 additions & 1 deletion src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2020,7 +2020,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
McpToolAvailability: extHostTypes.McpToolAvailability,
McpToolInvocationContentData: extHostTypes.McpToolInvocationContentData,
SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind,
ChatHookResultKind: extHostTypes.ChatHookResultKind,
ChatTodoStatus: extHostTypes.ChatTodoStatus,
};
};
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ import { IExtHostDocumentSaveDelegate } from './extHostDocumentData.js';
import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
import * as tasks from './shared/tasks.js';
import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js';
import { IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
import { IHookCommandResult, IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
import { IHookCommand } from '../../contrib/chat/common/promptSyntax/hookSchema.js';

export type IconPathDto =
Expand Down Expand Up @@ -3202,7 +3202,7 @@ export interface IStartMcpOptions {
export type IHookCommandDto = Dto<IHookCommand>;

export interface ExtHostHooksShape {
$runHookCommand(hookCommand: IHookCommandDto, input: unknown, token: CancellationToken): Promise<IHookResult>;
$runHookCommand(hookCommand: IHookCommandDto, input: unknown, token: CancellationToken): Promise<IHookCommandResult>;
}

export interface ExtHostMcpShape {
Expand Down
10 changes: 5 additions & 5 deletions src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCom
import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js';
import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js';
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { HookResultKind, IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
import { IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
import { IToolInvocationContext, IToolResult, IToolResultInputOutputDetails, IToolResultOutputDetails, ToolDataSource, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js';
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js';
Expand Down Expand Up @@ -4016,10 +4016,10 @@ export namespace SourceControlInputBoxValidationType {
export namespace ChatHookResult {
export function to(result: IHookResult): vscode.ChatHookResult {
return {
kind: result.kind === HookResultKind.Success
? types.ChatHookResultKind.Success
: types.ChatHookResultKind.Error,
result: result.result
stopReason: result.stopReason,
messageForUser: result.messageForUser,
output: result.output,
success: result.success,
};
}
}
9 changes: 0 additions & 9 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3935,15 +3935,6 @@ export enum SettingsSearchResultKind {

//#endregion

//#region Chat Hooks

export enum ChatHookResultKind {
Success = 1,
Error = 2
}

//#endregion

//#region Speech

export enum SpeechToTextStatus {
Expand Down
17 changes: 7 additions & 10 deletions src/vs/workbench/api/node/extHostHooksNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { isToolInvocationContext, IToolInvocationContext } from '../../contrib/c
import { IHookCommandDto, MainContext, MainThreadHooksShape } from '../common/extHost.protocol.js';
import { IChatHookExecutionOptions, IExtHostHooks } from '../common/extHostHooks.js';
import { IExtHostRpcService } from '../common/extHostRpcService.js';
import { HookResultKind, IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
import { HookCommandResultKind, IHookCommandResult, IHookResult } from '../../contrib/chat/common/hooksExecutionService.js';
import * as typeConverters from '../common/extHostTypeConverters.js';

const SIGKILL_DELAY_MS = 5000;
Expand All @@ -40,26 +40,23 @@ export class NodeExtHostHooks implements IExtHostHooks {
const context = options.toolInvocationToken as IToolInvocationContext;

const results = await this._mainThreadProxy.$executeHook(hookType, context.sessionResource, options.input, token ?? CancellationToken.None);
return results.map(r => typeConverters.ChatHookResult.to({
kind: r.kind as HookResultKind,
result: r.result
}));
return results.map(r => typeConverters.ChatHookResult.to(r as IHookResult));
}

async $runHookCommand(hookCommand: IHookCommandDto, input: unknown, token: CancellationToken): Promise<IHookResult> {
async $runHookCommand(hookCommand: IHookCommandDto, input: unknown, token: CancellationToken): Promise<IHookCommandResult> {
this._logService.debug(`[ExtHostHooks] Running hook command: ${JSON.stringify(hookCommand)}`);

try {
return await this._executeCommand(hookCommand, input, token);
} catch (err) {
return {
kind: HookResultKind.Error,
kind: HookCommandResultKind.Error,
result: err instanceof Error ? err.message : String(err)
};
}
}

private _executeCommand(hook: IHookCommandDto, input: unknown, token?: CancellationToken): Promise<IHookResult> {
private _executeCommand(hook: IHookCommandDto, input: unknown, token?: CancellationToken): Promise<IHookCommandResult> {
const home = homedir();
const cwdUri = hook.cwd ? URI.revive(hook.cwd) : undefined;
const cwd = cwdUri ? cwdUri.fsPath : home;
Expand Down Expand Up @@ -157,10 +154,10 @@ export class NodeExtHostHooks implements IExtHostHooks {
} catch {
// Keep as string if not valid JSON
}
resolve({ kind: HookResultKind.Success, result });
resolve({ kind: HookCommandResultKind.Success, result });
} else {
// Error
resolve({ kind: HookResultKind.Error, result: stderrStr });
resolve({ kind: HookCommandResultKind.Error, result: stderrStr });
}
});

Expand Down
18 changes: 9 additions & 9 deletions src/vs/workbench/api/test/node/extHostHooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c
import { NullLogService } from '../../../../platform/log/common/log.js';
import { NodeExtHostHooks } from '../../node/extHostHooksNode.js';
import { IHookCommandDto, MainThreadHooksShape } from '../../common/extHost.protocol.js';
import { IHookResult, HookResultKind } from '../../../contrib/chat/common/hooksExecutionService.js';
import { HookCommandResultKind, IHookResult } from '../../../contrib/chat/common/hooksExecutionService.js';
import { IExtHostRpcService } from '../../common/extHostRpcService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';

Expand Down Expand Up @@ -57,30 +57,30 @@ suite.skip('ExtHostHooks', () => {
const hookCommand = createHookCommandDto('echo "hello world"');
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Success);
assert.strictEqual(result.kind, HookCommandResultKind.Success);
assert.strictEqual((result.result as string).trim(), 'hello world');
});

test('$runHookCommand parses JSON output', async () => {
const hookCommand = createHookCommandDto('echo \'{"key": "value"}\'');
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Success);
assert.strictEqual(result.kind, HookCommandResultKind.Success);
assert.deepStrictEqual(result.result, { key: 'value' });
});

test('$runHookCommand returns error result for non-zero exit code', async () => {
const hookCommand = createHookCommandDto('exit 1');
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Error);
assert.strictEqual(result.kind, HookCommandResultKind.Error);
});

test('$runHookCommand captures stderr on failure', async () => {
const hookCommand = createHookCommandDto('echo "error message" >&2 && exit 1');
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Error);
assert.strictEqual(result.kind, HookCommandResultKind.Error);
assert.strictEqual((result.result as string).trim(), 'error message');
});

Expand All @@ -89,30 +89,30 @@ suite.skip('ExtHostHooks', () => {
const input = { tool: 'bash', args: { command: 'ls' } };
const result = await hooksService.$runHookCommand(hookCommand, input, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Success);
assert.strictEqual(result.kind, HookCommandResultKind.Success);
assert.deepStrictEqual(result.result, input);
});

test('$runHookCommand returns error for invalid command', async () => {
const hookCommand = createHookCommandDto('/nonexistent/command/that/does/not/exist');
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Error);
assert.strictEqual(result.kind, HookCommandResultKind.Error);
});

test('$runHookCommand uses custom environment variables', async () => {
const hookCommand = createHookCommandDto('echo $MY_VAR', { env: { MY_VAR: 'custom_value' } });
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Success);
assert.strictEqual(result.kind, HookCommandResultKind.Success);
assert.strictEqual((result.result as string).trim(), 'custom_value');
});

test('$runHookCommand uses custom cwd', async () => {
const hookCommand = createHookCommandDto('pwd', { cwd: URI.file('/tmp') });
const result = await hooksService.$runHookCommand(hookCommand, undefined, CancellationToken.None);

assert.strictEqual(result.kind, HookResultKind.Success);
assert.strictEqual(result.kind, HookCommandResultKind.Success);
// The result should contain /tmp or /private/tmp (macOS symlink)
assert.ok((result.result as string).includes('tmp'));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILangu
import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js';
import { URI } from '../../../../../base/common/uri.js';
import { chatSessionResourceToId } from '../../common/model/chatUri.js';
import { HookType } from '../../common/promptSyntax/hookSchema.js';

const jsonSchemaRegistry = Registry.as<JSONContributionRegistry.IJSONContributionRegistry>(JSONContributionRegistry.Extensions.JSONContribution);

Expand Down Expand Up @@ -384,7 +385,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo

if (hookResult?.permissionDecision === 'deny') {
const hookReason = hookResult.permissionDecisionReason ?? localize('hookDeniedNoReason', "Hook denied tool execution");
const reason = localize('deniedByPreToolUseHook', "Denied by {0} hook: {1}", 'preToolUse', hookReason);
const reason = localize('deniedByPreToolUseHook', "Denied by {0} hook: {1}", HookType.PreToolUse, hookReason);
this._logService.debug(`[LanguageModelToolsService#invokeTool] Tool ${dto.toolId} denied by preToolUse hook: ${hookReason}`);

// Handle the tool invocation in cancelled state
Expand Down
Loading
Loading