@@ -11,7 +11,6 @@ import { ConfigurationTarget } from '../../../../../../platform/configuration/co
11
11
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js' ;
12
12
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js' ;
13
13
import { IToolInvocationPreparationContext , IPreparedToolInvocation , ILanguageModelToolsService } from '../../../../chat/common/languageModelToolsService.js' ;
14
- import { CommandLineAutoApprover } from '../../browser/commandLineAutoApprover.js' ;
15
14
import { RunInTerminalTool , type IRunInTerminalInputParams } from '../../browser/runInTerminalTool.js' ;
16
15
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js' ;
17
16
import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js' ;
@@ -20,11 +19,14 @@ import type { TestInstantiationService } from '../../../../../../platform/instan
20
19
import { ITerminalService , type ITerminalInstance } from '../../../../terminal/browser/terminal.js' ;
21
20
import { OperatingSystem } from '../../../../../../base/common/platform.js' ;
22
21
import { Emitter } from '../../../../../../base/common/event.js' ;
22
+ import { IChatService } from '../../../../chat/common/chatService.js' ;
23
+ import { ShellIntegrationQuality } from '../../browser/toolTerminalCreator.js' ;
23
24
24
25
class TestRunInTerminalTool extends RunInTerminalTool {
25
26
protected override _osBackend : Promise < OperatingSystem > = Promise . resolve ( OperatingSystem . Windows ) ;
26
27
27
- get commandLineAutoApprover ( ) : CommandLineAutoApprover { return this . _commandLineAutoApprover ; }
28
+ get commandLineAutoApprover ( ) { return this . _commandLineAutoApprover ; }
29
+ get sessionTerminalAssociations ( ) { return this . _sessionTerminalAssociations ; }
28
30
29
31
async rewriteCommandIfNeeded ( args : IRunInTerminalInputParams , instance : Pick < ITerminalInstance , 'getCwdResource' > | undefined , shell : string ) : Promise < string > {
30
32
return this . _rewriteCommandIfNeeded ( args , instance , shell ) ;
@@ -41,11 +43,16 @@ suite('RunInTerminalTool', () => {
41
43
let instantiationService : TestInstantiationService ;
42
44
let configurationService : TestConfigurationService ;
43
45
let workspaceService : TestContextService ;
46
+ let terminalServiceDisposeEmitter : Emitter < ITerminalInstance > ;
47
+ let chatServiceDisposeEmitter : Emitter < { sessionId : string ; reason : 'cleared' } > ;
44
48
45
49
let runInTerminalTool : TestRunInTerminalTool ;
46
50
47
51
setup ( ( ) => {
48
52
configurationService = new TestConfigurationService ( ) ;
53
+ terminalServiceDisposeEmitter = new Emitter < ITerminalInstance > ( ) ;
54
+ chatServiceDisposeEmitter = new Emitter < { sessionId : string ; reason : 'cleared' } > ( ) ;
55
+
49
56
instantiationService = workbenchInstantiationService ( {
50
57
configurationService : ( ) => configurationService ,
51
58
} , store ) ;
@@ -55,7 +62,10 @@ suite('RunInTerminalTool', () => {
55
62
} ,
56
63
} ) ;
57
64
instantiationService . stub ( ITerminalService , {
58
- onDidDisposeInstance : new Emitter < ITerminalInstance > ( ) . event
65
+ onDidDisposeInstance : terminalServiceDisposeEmitter . event
66
+ } ) ;
67
+ instantiationService . stub ( IChatService , {
68
+ onDidDisposeSession : chatServiceDisposeEmitter . event
59
69
} ) ;
60
70
workspaceService = instantiationService . invokeFunction ( accessor => accessor . get ( IWorkspaceContextService ) ) as TestContextService ;
61
71
@@ -611,4 +621,71 @@ suite('RunInTerminalTool', () => {
611
621
} ) ;
612
622
} ) ;
613
623
} ) ;
624
+
625
+ suite ( 'chat session disposal cleanup' , ( ) => {
626
+ test ( 'should dispose associated terminals when chat session is disposed' , ( ) => {
627
+ const sessionId = 'test-session-123' ;
628
+ const mockTerminal : ITerminalInstance = {
629
+ dispose : ( ) => { /* Mock dispose */ } ,
630
+ processId : 12345
631
+ } as any ;
632
+ let terminalDisposed = false ;
633
+ mockTerminal . dispose = ( ) => { terminalDisposed = true ; } ;
634
+
635
+ runInTerminalTool . sessionTerminalAssociations . set ( sessionId , {
636
+ instance : mockTerminal ,
637
+ shellIntegrationQuality : ShellIntegrationQuality . None
638
+ } ) ;
639
+
640
+ ok ( runInTerminalTool . sessionTerminalAssociations . has ( sessionId ) , 'Terminal association should exist before disposal' ) ;
641
+
642
+ chatServiceDisposeEmitter . fire ( { sessionId, reason : 'cleared' } ) ;
643
+
644
+ strictEqual ( terminalDisposed , true , 'Terminal should have been disposed' ) ;
645
+ ok ( ! runInTerminalTool . sessionTerminalAssociations . has ( sessionId ) , 'Terminal association should be removed after disposal' ) ;
646
+ } ) ;
647
+
648
+ test ( 'should not affect other sessions when one session is disposed' , ( ) => {
649
+ const sessionId1 = 'test-session-1' ;
650
+ const sessionId2 = 'test-session-2' ;
651
+ const mockTerminal1 : ITerminalInstance = {
652
+ dispose : ( ) => { /* Mock dispose */ } ,
653
+ processId : 12345
654
+ } as any ;
655
+ const mockTerminal2 : ITerminalInstance = {
656
+ dispose : ( ) => { /* Mock dispose */ } ,
657
+ processId : 67890
658
+ } as any ;
659
+
660
+ let terminal1Disposed = false ;
661
+ let terminal2Disposed = false ;
662
+ mockTerminal1 . dispose = ( ) => { terminal1Disposed = true ; } ;
663
+ mockTerminal2 . dispose = ( ) => { terminal2Disposed = true ; } ;
664
+
665
+ runInTerminalTool . sessionTerminalAssociations . set ( sessionId1 , {
666
+ instance : mockTerminal1 ,
667
+ shellIntegrationQuality : ShellIntegrationQuality . None
668
+ } ) ;
669
+ runInTerminalTool . sessionTerminalAssociations . set ( sessionId2 , {
670
+ instance : mockTerminal2 ,
671
+ shellIntegrationQuality : ShellIntegrationQuality . None
672
+ } ) ;
673
+
674
+ ok ( runInTerminalTool . sessionTerminalAssociations . has ( sessionId1 ) , 'Session 1 terminal association should exist' ) ;
675
+ ok ( runInTerminalTool . sessionTerminalAssociations . has ( sessionId2 ) , 'Session 2 terminal association should exist' ) ;
676
+
677
+ chatServiceDisposeEmitter . fire ( { sessionId : sessionId1 , reason : 'cleared' } ) ;
678
+
679
+ strictEqual ( terminal1Disposed , true , 'Terminal 1 should have been disposed' ) ;
680
+ strictEqual ( terminal2Disposed , false , 'Terminal 2 should NOT have been disposed' ) ;
681
+ ok ( ! runInTerminalTool . sessionTerminalAssociations . has ( sessionId1 ) , 'Session 1 terminal association should be removed' ) ;
682
+ ok ( runInTerminalTool . sessionTerminalAssociations . has ( sessionId2 ) , 'Session 2 terminal association should remain' ) ;
683
+ } ) ;
684
+
685
+ test ( 'should handle disposal of non-existent session gracefully' , ( ) => {
686
+ strictEqual ( runInTerminalTool . sessionTerminalAssociations . size , 0 , 'No associations should exist initially' ) ;
687
+ chatServiceDisposeEmitter . fire ( { sessionId : 'non-existent-session' , reason : 'cleared' } ) ;
688
+ strictEqual ( runInTerminalTool . sessionTerminalAssociations . size , 0 , 'No associations should exist after handling non-existent session' ) ;
689
+ } ) ;
690
+ } ) ;
614
691
} ) ;
0 commit comments