33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
55
6- import { Options , Query , SDKAssistantMessage , SDKResultMessage , SDKUserMessage } from '@anthropic-ai/claude-code' ;
6+ import { HookInput , HookJSONOutput , Options , PreToolUseHookInput , Query , SDKAssistantMessage , SDKResultMessage , SDKUserMessage } from '@anthropic-ai/claude-code' ;
77import Anthropic from '@anthropic-ai/sdk' ;
88import type * as vscode from 'vscode' ;
99import { ConfigKey , IConfigurationService } from '../../../../platform/configuration/common/configurationService' ;
@@ -22,7 +22,7 @@ import { ToolName } from '../../../tools/common/toolNames';
2222import { IToolsService } from '../../../tools/common/toolsService' ;
2323import { isFileOkForTool } from '../../../tools/node/toolUtils' ;
2424import { ILanguageModelServerConfig , LanguageModelServer } from '../../node/langModelServer' ;
25- import { ClaudeToolNames , IExitPlanModeInput , ITodoWriteInput } from '../common/claudeTools' ;
25+ import { claudeEditTools , ClaudeToolNames , getAffectedUrisForEditTool , IExitPlanModeInput , ITodoWriteInput } from '../common/claudeTools' ;
2626import { createFormattedToolInvocation } from '../common/toolInvocationFormatter' ;
2727import { IClaudeCodeSdkService } from './claudeCodeSdkService' ;
2828
@@ -153,6 +153,7 @@ export class ClaudeCodeSession extends Disposable {
153153 private _currentRequest : CurrentRequest | undefined ;
154154 private _pendingPrompt : DeferredPromise < QueuedRequest > | undefined ;
155155 private _abortController = new AbortController ( ) ;
156+ private _ongoingEdits = new Map < string | undefined , { complete : ( ) => void ; onDidComplete : Thenable < void > } > ( ) ;
156157
157158 constructor (
158159 private readonly serverConfig : ILanguageModelServerConfig ,
@@ -163,7 +164,8 @@ export class ClaudeCodeSession extends Disposable {
163164 @IEnvService private readonly envService : IEnvService ,
164165 @IInstantiationService private readonly instantiationService : IInstantiationService ,
165166 @IToolsService private readonly toolsService : IToolsService ,
166- @IClaudeCodeSdkService private readonly claudeCodeService : IClaudeCodeSdkService
167+ @IClaudeCodeSdkService private readonly claudeCodeService : IClaudeCodeSdkService ,
168+ @ILogService private readonly _log : ILogService ,
167169 ) {
168170 super ( ) ;
169171 }
@@ -195,7 +197,7 @@ export class ClaudeCodeSession extends Disposable {
195197 }
196198
197199 if ( ! this . _queryGenerator ) {
198- await this . _startSession ( ) ;
200+ await this . _startSession ( token ) ;
199201 }
200202
201203 // Add this request to the queue and wait for completion
@@ -232,7 +234,7 @@ export class ClaudeCodeSession extends Disposable {
232234 /**
233235 * Starts a new Claude Code session with the configured options
234236 */
235- private async _startSession ( ) : Promise < void > {
237+ private async _startSession ( token : vscode . CancellationToken ) : Promise < void > {
236238 // Build options for the Claude Code SDK
237239 // process.env.DEBUG = '1'; // debug messages from sdk.mjs
238240 const isDebugEnabled = this . configService . getConfig ( ConfigKey . Internal . ClaudeCodeDebugEnabled ) ;
@@ -252,6 +254,20 @@ export class ClaudeCodeSession extends Disposable {
252254 PATH : `${ this . envService . appRoot } /node_modules/@vscode/ripgrep/bin${ pathSep } ${ process . env . PATH } `
253255 } ,
254256 resume : this . sessionId ,
257+ hooks : {
258+ PreToolUse : [
259+ {
260+ matcher : claudeEditTools . join ( '|' ) ,
261+ hooks : [ ( input , toolID ) => this . _onWillEditTool ( input , toolID , token ) ]
262+ }
263+ ] ,
264+ PostToolUse : [
265+ {
266+ matcher : claudeEditTools . join ( '|' ) ,
267+ hooks : [ ( input , toolID ) => this . _onDidEditTool ( input , toolID ) ]
268+ }
269+ ] ,
270+ } ,
255271 canUseTool : async ( name , input ) => {
256272 return this . _currentRequest ?
257273 this . canUseTool ( name , input , this . _currentRequest . toolInvocationToken ) :
@@ -270,6 +286,47 @@ export class ClaudeCodeSession extends Disposable {
270286 this . _processMessages ( ) ;
271287 }
272288
289+ private async _onWillEditTool ( input : HookInput , toolUseID : string | undefined , token : CancellationToken ) : Promise < HookJSONOutput > {
290+ let uris : URI [ ] = [ ] ;
291+ try {
292+ uris = getAffectedUrisForEditTool ( input as PreToolUseHookInput ) ;
293+ } catch ( error ) {
294+ this . _log . error ( 'Error getting affected URIs for edit tool' , error ) ;
295+ }
296+ if ( ! uris . length || ! this . _currentRequest || token . isCancellationRequested ) {
297+ return { } ;
298+ }
299+
300+ if ( ! this . _currentRequest ! . stream . externalEdit ) {
301+ return { } ; // back-compat during 1.106 insiders
302+ }
303+
304+ return new Promise ( proceedWithEdit => {
305+ const deferred = new DeferredPromise < void > ( ) ;
306+ const cancelListen = token . onCancellationRequested ( ( ) => {
307+ this . _ongoingEdits . delete ( toolUseID ) ;
308+ deferred . complete ( ) ;
309+ } ) ;
310+ const onDidComplete = this . _currentRequest ! . stream . externalEdit ( uris , async ( ) => {
311+ proceedWithEdit ( { } ) ;
312+ await deferred . p ;
313+ cancelListen . dispose ( ) ;
314+ } ) ;
315+
316+ this . _ongoingEdits . set ( toolUseID , { onDidComplete, complete : ( ) => deferred . complete ( ) } ) ;
317+ } ) ;
318+ }
319+
320+ private async _onDidEditTool ( input : HookInput , toolUseID : string | undefined ) {
321+ const ongoingEdit = this . _ongoingEdits . get ( toolUseID ) ;
322+ if ( ongoingEdit ) {
323+ this . _ongoingEdits . delete ( toolUseID ) ;
324+ ongoingEdit . complete ( ) ;
325+ await ongoingEdit . onDidComplete ;
326+ }
327+ return { } ;
328+ }
329+
273330 private async * _createPromptIterable ( ) : AsyncIterable < SDKUserMessage > {
274331 while ( true ) {
275332 // Wait for a request to be available
0 commit comments