@@ -16,6 +16,7 @@ import { ITerminalChatService, type ITerminalInstance } from '../../../../termin
1616import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js' ;
1717import type { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js' ;
1818import { IChatService } from '../../../../chat/common/chatService/chatService.js' ;
19+ import { URI } from '../../../../../../base/common/uri.js' ;
1920
2021suite ( 'SendToTerminalTool' , ( ) => {
2122 const store = ensureNoDisposablesAreLeakedInTestSuite ( ) ;
@@ -134,10 +135,11 @@ suite('SendToTerminalTool', () => {
134135 assert . strictEqual ( mockExecution . sentTexts [ 0 ] . shouldExecute , true ) ;
135136 } ) ;
136137
137- function createPreparationContext ( id : string , command : string ) : IToolInvocationPreparationContext {
138+ function createPreparationContext ( id : string , command : string , chatSessionResource ?: URI ) : IToolInvocationPreparationContext {
138139 return {
139140 parameters : { id, command } ,
140141 toolCallId : 'test-call' ,
142+ chatSessionResource,
141143 } as unknown as IToolInvocationPreparationContext ;
142144 }
143145
@@ -177,4 +179,176 @@ suite('SendToTerminalTool', () => {
177179 const message = prepared . invocationMessage as IMarkdownString ;
178180 assert . ok ( ! message . value . includes ( '\n' ) , 'newlines should be collapsed to spaces' ) ;
179181 } ) ;
182+
183+ test ( 'prepareToolInvocation skips confirmation when answering a question carousel' , async ( ) => {
184+ const sessionResource = URI . parse ( 'chat-session://test-session' ) ;
185+ const mockSession = {
186+ getRequests : ( ) => [ {
187+ response : {
188+ response : {
189+ value : [ {
190+ kind : 'questionCarousel' as const ,
191+ terminalId : KNOWN_TERMINAL_ID ,
192+ questions : [ { id : 'q1' , title : 'package name?' , message : 'package name?' } ] ,
193+ data : { q1 : 'my-package' } ,
194+ } ]
195+ }
196+ }
197+ } ] ,
198+ } ;
199+ instantiationService . stub ( IChatService , 'getSession' , ( ) => mockSession ) ;
200+ tool = store . add ( instantiationService . createInstance ( SendToTerminalTool ) ) ;
201+
202+ const prepared = await tool . prepareToolInvocation (
203+ createPreparationContext ( KNOWN_TERMINAL_ID , 'my-package' , sessionResource ) ,
204+ CancellationToken . None ,
205+ ) ;
206+
207+ assert . ok ( prepared ) ;
208+ assert . strictEqual ( prepared . confirmationMessages , undefined , 'should skip confirmation when the command matches a carousel answer' ) ;
209+ } ) ;
210+
211+ test ( 'prepareToolInvocation does not skip confirmation when the command does not match a carousel answer' , async ( ) => {
212+ const sessionResource = URI . parse ( 'chat-session://test-session' ) ;
213+ const mockSession = {
214+ getRequests : ( ) => [ {
215+ response : {
216+ response : {
217+ value : [ {
218+ kind : 'questionCarousel' as const ,
219+ terminalId : KNOWN_TERMINAL_ID ,
220+ questions : [ { id : 'q1' , title : 'package name?' , message : 'package name?' } ] ,
221+ data : { q1 : 'my-package' } ,
222+ } ]
223+ }
224+ }
225+ } ] ,
226+ } ;
227+ instantiationService . stub ( IChatService , 'getSession' , ( ) => mockSession ) ;
228+ tool = store . add ( instantiationService . createInstance ( SendToTerminalTool ) ) ;
229+
230+ const prepared = await tool . prepareToolInvocation (
231+ createPreparationContext ( KNOWN_TERMINAL_ID , 'different-package' , sessionResource ) ,
232+ CancellationToken . None ,
233+ ) ;
234+
235+ assert . ok ( prepared ) ;
236+ assert . ok ( prepared . confirmationMessages , 'should require confirmation when the command does not match a carousel answer' ) ;
237+ } ) ;
238+
239+ test ( 'prepareToolInvocation skips confirmation only for exact matches in multi-question carousels' , async ( ) => {
240+ const sessionResource = URI . parse ( 'chat-session://test-session' ) ;
241+ const carousel = {
242+ kind : 'questionCarousel' as const ,
243+ terminalId : KNOWN_TERMINAL_ID ,
244+ questions : [
245+ { id : 'q1' , title : 'package name?' , message : 'package name?' } ,
246+ { id : 'q2' , title : 'entry point?' , message : 'entry point?' }
247+ ] ,
248+ data : { q1 : 'my-package' , q2 : 'src/index.ts' } ,
249+ } ;
250+ // Simulate one prior send_to_terminal invocation after the carousel
251+ // so that positional matching targets question[1] (entry point)
252+ const priorSendInvocation = {
253+ kind : 'toolInvocation' as const ,
254+ toolId : 'send_to_terminal' ,
255+ } ;
256+ const mockSession = {
257+ getRequests : ( ) => [ {
258+ response : {
259+ response : {
260+ value : [ carousel , priorSendInvocation ]
261+ }
262+ }
263+ } ] ,
264+ } ;
265+ instantiationService . stub ( IChatService , 'getSession' , ( ) => mockSession ) ;
266+ tool = store . add ( instantiationService . createInstance ( SendToTerminalTool ) ) ;
267+
268+ const exactMatchPrepared = await tool . prepareToolInvocation (
269+ createPreparationContext ( KNOWN_TERMINAL_ID , 'src/index.ts' , sessionResource ) ,
270+ CancellationToken . None ,
271+ ) ;
272+
273+ assert . ok ( exactMatchPrepared ) ;
274+ assert . strictEqual ( exactMatchPrepared . confirmationMessages , undefined , 'should skip confirmation when the command exactly matches a carousel answer' ) ;
275+
276+ const mismatchedPrepared = await tool . prepareToolInvocation (
277+ createPreparationContext ( KNOWN_TERMINAL_ID , 'src/index.js' , sessionResource ) ,
278+ CancellationToken . None ,
279+ ) ;
280+
281+ assert . ok ( mismatchedPrepared ) ;
282+ assert . ok ( mismatchedPrepared . confirmationMessages , 'should require confirmation when the command does not exactly match any carousel answer' ) ;
283+ } ) ;
284+
285+ test ( 'prepareToolInvocation uses positional matching for identical answers (all defaults)' , async ( ) => {
286+ const sessionResource = URI . parse ( 'chat-session://test-session' ) ;
287+ const carousel = {
288+ kind : 'questionCarousel' as const ,
289+ terminalId : KNOWN_TERMINAL_ID ,
290+ questions : [
291+ { id : 'q1' , title : 'package name?' , message : 'package name?' } ,
292+ { id : 'q2' , title : 'version?' , message : 'version?' } ,
293+ { id : 'q3' , title : 'description?' , message : 'description?' } ,
294+ ] ,
295+ data : { q1 : '' , q2 : '' , q3 : '' } ,
296+ } ;
297+
298+ // First call: no prior send_to_terminal → positional index 0 → "package name?"
299+ const mockSession0 = {
300+ getRequests : ( ) => [ {
301+ response : { response : { value : [ carousel ] } }
302+ } ] ,
303+ } ;
304+ instantiationService . stub ( IChatService , 'getSession' , ( ) => mockSession0 ) ;
305+ tool = store . add ( instantiationService . createInstance ( SendToTerminalTool ) ) ;
306+
307+ const first = await tool . prepareToolInvocation (
308+ createPreparationContext ( KNOWN_TERMINAL_ID , '' , sessionResource ) ,
309+ CancellationToken . None ,
310+ ) ;
311+ assert . ok ( first ) ;
312+ assert . strictEqual ( first . confirmationMessages , undefined ) ;
313+ const firstMsg = first . pastTenseMessage as IMarkdownString ;
314+ assert . ok ( firstMsg . value . includes ( 'package' ) , 'first call should show package name question' ) ;
315+
316+ // Second call: one prior send_to_terminal → positional index 1 → "version?"
317+ const priorSend1 = { kind : 'toolInvocation' as const , toolId : 'send_to_terminal' } ;
318+ const mockSession1 = {
319+ getRequests : ( ) => [ {
320+ response : { response : { value : [ carousel , priorSend1 ] } }
321+ } ] ,
322+ } ;
323+ instantiationService . stub ( IChatService , 'getSession' , ( ) => mockSession1 ) ;
324+ tool = store . add ( instantiationService . createInstance ( SendToTerminalTool ) ) ;
325+
326+ const second = await tool . prepareToolInvocation (
327+ createPreparationContext ( KNOWN_TERMINAL_ID , '' , sessionResource ) ,
328+ CancellationToken . None ,
329+ ) ;
330+ assert . ok ( second ) ;
331+ assert . strictEqual ( second . confirmationMessages , undefined ) ;
332+ const secondMsg = second . pastTenseMessage as IMarkdownString ;
333+ assert . ok ( secondMsg . value . includes ( 'version' ) , 'second call should show version question' ) ;
334+
335+ // Third call: two prior send_to_terminal → positional index 2 → "description?"
336+ const priorSend2 = { kind : 'toolInvocation' as const , toolId : 'send_to_terminal' } ;
337+ const mockSession2 = {
338+ getRequests : ( ) => [ {
339+ response : { response : { value : [ carousel , priorSend1 , priorSend2 ] } }
340+ } ] ,
341+ } ;
342+ instantiationService . stub ( IChatService , 'getSession' , ( ) => mockSession2 ) ;
343+ tool = store . add ( instantiationService . createInstance ( SendToTerminalTool ) ) ;
344+
345+ const third = await tool . prepareToolInvocation (
346+ createPreparationContext ( KNOWN_TERMINAL_ID , '' , sessionResource ) ,
347+ CancellationToken . None ,
348+ ) ;
349+ assert . ok ( third ) ;
350+ assert . strictEqual ( third . confirmationMessages , undefined ) ;
351+ const thirdMsg = third . pastTenseMessage as IMarkdownString ;
352+ assert . ok ( thirdMsg . value . includes ( 'description' ) , 'third call should show description question' ) ;
353+ } ) ;
180354} ) ;
0 commit comments