@@ -53,6 +53,7 @@ import { randomUUID } from '../../../shared/crypto'
5353import { tempDirPath , testGenerationLogsDir } from '../../../shared/filesystemUtilities'
5454import { CodeReference } from '../../../codewhispererChat/view/connector/connector'
5555import { TelemetryHelper } from '../../../codewhisperer/util/telemetryHelper'
56+ import { QCliStreamingService } from '../streaming/qCliStreamingService'
5657import { Reference , testGenState } from '../../../codewhisperer/models/model'
5758import {
5859 referenceLogText ,
@@ -111,6 +112,9 @@ export class TestController {
111112 this . authController = new AuthController ( )
112113 this . editorContentController = new EditorContentController ( )
113114
115+ // Initialize the Q CLI streaming service
116+ QCliStreamingService . getInstance ( ) . initialize ( messenger )
117+
114118 this . chatControllerMessageListeners . tabOpened . event ( ( data ) => {
115119 return this . tabOpened ( data )
116120 } )
@@ -218,6 +222,12 @@ export class TestController {
218222 session . isAuthenticating = true
219223 return
220224 }
225+
226+ // Update placeholder to indicate Q CLI-like functionality
227+ this . messenger . sendUpdatePlaceholder (
228+ tabID ,
229+ 'Ask Amazon Q a question or use /test to generate unit tests...'
230+ )
221231 } catch ( err : any ) {
222232 logger . error ( 'tabOpened failed: %O' , err )
223233 this . messenger . sendErrorMessage ( err . message , message . tabID )
@@ -413,8 +423,12 @@ export class TestController {
413423 this . messenger . sendMessage ( `Updated command to \`${ updatedCommands } \`` , data . tabID , 'prompt' )
414424 await this . checkForInstallationDependencies ( data )
415425 return
416- } else {
426+ } else if ( data . prompt . startsWith ( '/test' ) ) {
427+ // If the prompt starts with /test, use the original test generation flow
417428 await this . startTestGen ( data , false )
429+ } else {
430+ // Otherwise, use the Q CLI streaming functionality
431+ await this . processQCliStreamingMessage ( data )
418432 }
419433 }
420434 // This function takes filePath as input parameter and returns file language
@@ -474,124 +488,127 @@ export class TestController {
474488 )
475489 return
476490 }
477- // Truncating the user prompt if the prompt is more than 4096.
478- userPrompt = message . prompt . slice ( 0 , maxUserPromptLength )
479-
480- // check that the session is authenticated
481- const authState = await AuthUtil . instance . getChatAuthState ( )
482- if ( authState . amazonQ !== 'connected' ) {
483- void this . messenger . sendAuthNeededExceptionMessage ( authState , tabID )
484- session . isAuthenticating = true
485- return
486- }
491+ await this . processQCliStreamingMessage ( message )
492+ if ( session . listOfTestGenerationJobId . length > 1 ) {
493+ // Truncating the user prompt if the prompt is more than 4096.
494+ userPrompt = message . prompt . slice ( 0 , maxUserPromptLength )
495+
496+ // check that the session is authenticated
497+ const authState = await AuthUtil . instance . getChatAuthState ( )
498+ if ( authState . amazonQ !== 'connected' ) {
499+ void this . messenger . sendAuthNeededExceptionMessage ( authState , tabID )
500+ session . isAuthenticating = true
501+ return
502+ }
487503
488- // check that a project/workspace is open
489- const workspaceFolders = vscode . workspace . workspaceFolders
490- if ( workspaceFolders === undefined || workspaceFolders . length === 0 ) {
491- this . messenger . sendUnrecoverableErrorResponse ( 'no-project-found' , tabID )
492- return
493- }
504+ // check that a project/workspace is open
505+ const workspaceFolders = vscode . workspace . workspaceFolders
506+ if ( workspaceFolders === undefined || workspaceFolders . length === 0 ) {
507+ this . messenger . sendUnrecoverableErrorResponse ( 'no-project-found' , tabID )
508+ return
509+ }
494510
495- // check if IDE has active file open.
496- const activeEditor = vscode . window . activeTextEditor
497- // also check all open editors and allow this to proceed if only one is open (even if not main focus)
498- const allVisibleEditors = vscode . window . visibleTextEditors
499- const openFileEditors = allVisibleEditors . filter ( ( editor ) => editor . document . uri . scheme === 'file' )
500- const hasOnlyOneOpenFileSplitView = openFileEditors . length === 1
501- getLogger ( ) . debug ( `hasOnlyOneOpenSplitView: ${ hasOnlyOneOpenFileSplitView } ` )
502- // is not a file if the currently highlighted window is not a file, and there is either more than one or no file windows open
503- const isNotFile = activeEditor ?. document . uri . scheme !== 'file' && ! hasOnlyOneOpenFileSplitView
504- getLogger ( ) . debug ( `activeEditor: ${ activeEditor } , isNotFile: ${ isNotFile } ` )
505- if ( ! activeEditor || isNotFile ) {
506- this . messenger . sendUnrecoverableErrorResponse (
507- isNotFile ? 'invalid-file-type' : 'no-open-file-found' ,
508- tabID
509- )
510- this . messenger . sendUpdatePlaceholder (
511- tabID ,
512- 'Please open and highlight a source code file in order to generate tests.'
513- )
514- this . messenger . sendChatInputEnabled ( tabID , true )
515- this . sessionStorage . getSession ( ) . conversationState = ConversationState . WAITING_FOR_INPUT
516- return
517- }
511+ // check if IDE has active file open.
512+ const activeEditor = vscode . window . activeTextEditor
513+ // also check all open editors and allow this to proceed if only one is open (even if not main focus)
514+ const allVisibleEditors = vscode . window . visibleTextEditors
515+ const openFileEditors = allVisibleEditors . filter ( ( editor ) => editor . document . uri . scheme === 'file' )
516+ const hasOnlyOneOpenFileSplitView = openFileEditors . length === 1
517+ getLogger ( ) . debug ( `hasOnlyOneOpenSplitView: ${ hasOnlyOneOpenFileSplitView } ` )
518+ // is not a file if the currently highlighted window is not a file, and there is either more than one or no file windows open
519+ const isNotFile = activeEditor ?. document . uri . scheme !== 'file' && ! hasOnlyOneOpenFileSplitView
520+ getLogger ( ) . debug ( `activeEditor: ${ activeEditor } , isNotFile: ${ isNotFile } ` )
521+ if ( ! activeEditor || isNotFile ) {
522+ this . messenger . sendUnrecoverableErrorResponse (
523+ isNotFile ? 'invalid-file-type' : 'no-open-file-found' ,
524+ tabID
525+ )
526+ this . messenger . sendUpdatePlaceholder (
527+ tabID ,
528+ 'Please open and highlight a source code file in order to generate tests.'
529+ )
530+ this . messenger . sendChatInputEnabled ( tabID , true )
531+ this . sessionStorage . getSession ( ) . conversationState = ConversationState . WAITING_FOR_INPUT
532+ return
533+ }
518534
519- const fileEditorToTest = hasOnlyOneOpenFileSplitView ? openFileEditors [ 0 ] : activeEditor
520- getLogger ( ) . debug ( `File path: ${ fileEditorToTest . document . uri . fsPath } ` )
521- filePath = fileEditorToTest . document . uri . fsPath
522- fileName = path . basename ( filePath )
523- userFacingMessage = userPrompt
524- ? regenerateTests
525- ? `${ userPrompt } `
526- : `/test ${ userPrompt } `
527- : `/test Generate unit tests for \`${ fileName } \``
528-
529- session . hasUserPromptSupplied = userPrompt . length > 0
530-
531- // displaying user message prompt in Test tab
532- this . messenger . sendMessage ( userFacingMessage , tabID , 'prompt' )
533- this . messenger . sendChatInputEnabled ( tabID , false )
534- this . sessionStorage . getSession ( ) . conversationState = ConversationState . IN_PROGRESS
535- this . messenger . sendUpdatePromptProgress ( message . tabID , testGenProgressField )
536-
537- const language = await this . getLanguageForFilePath ( filePath )
538- session . fileLanguage = language
539- const workspaceFolder = vscode . workspace . getWorkspaceFolder ( fileEditorToTest . document . uri )
540-
541- /*
535+ const fileEditorToTest = hasOnlyOneOpenFileSplitView ? openFileEditors [ 0 ] : activeEditor
536+ getLogger ( ) . debug ( `File path: ${ fileEditorToTest . document . uri . fsPath } ` )
537+ filePath = fileEditorToTest . document . uri . fsPath
538+ fileName = path . basename ( filePath )
539+ userFacingMessage = userPrompt
540+ ? regenerateTests
541+ ? `${ userPrompt } `
542+ : `/test ${ userPrompt } `
543+ : `/test Generate unit tests for \`${ fileName } \``
544+
545+ session . hasUserPromptSupplied = userPrompt . length > 0
546+
547+ // displaying user message prompt in Test tab
548+ this . messenger . sendMessage ( userFacingMessage , tabID , 'prompt' )
549+ this . messenger . sendChatInputEnabled ( tabID , false )
550+ this . sessionStorage . getSession ( ) . conversationState = ConversationState . IN_PROGRESS
551+ this . messenger . sendUpdatePromptProgress ( message . tabID , testGenProgressField )
552+
553+ const language = await this . getLanguageForFilePath ( filePath )
554+ session . fileLanguage = language
555+ const workspaceFolder = vscode . workspace . getWorkspaceFolder ( fileEditorToTest . document . uri )
556+
557+ /*
542558 For Re:Invent 2024 we are supporting only java and python for unit test generation, rest of the languages shows the similar experience as CWC
543559 */
544- if ( ! [ 'java' , 'python' ] . includes ( language ) || workspaceFolder === undefined ) {
545- let unsupportedMessage : string
546- const unsupportedLanguage = language ? language . charAt ( 0 ) . toUpperCase ( ) + language . slice ( 1 ) : ''
547- if ( ! workspaceFolder ) {
548- // File is outside of workspace
549- unsupportedMessage = `<span style="color: #EE9D28;">⚠<b>I can't generate tests for ${ fileName } </b> because the file is outside of workspace scope.<br></span> I can still provide examples, instructions and code suggestions.`
550- } else if ( unsupportedLanguage ) {
551- unsupportedMessage = `<span style="color: #EE9D28;">⚠<b>I'm sorry, but /test only supports Python and Java</b><br></span> While ${ unsupportedLanguage } is not supported, I will generate a suggestion below.`
552- } else {
553- unsupportedMessage = `<span style="color: #EE9D28;">⚠<b>I'm sorry, but /test only supports Python and Java</b><br></span> I will still generate a suggestion below.`
554- }
555- this . messenger . sendMessage ( unsupportedMessage , tabID , 'answer' )
556- session . isSupportedLanguage = false
557- await this . onCodeGeneration (
558- session ,
559- userPrompt ,
560- tabID ,
561- fileName ,
562- filePath ,
563- workspaceFolder !== undefined
564- )
565- } else {
566- this . messenger . sendCapabilityCard ( { tabID } )
567- this . messenger . sendMessage ( testGenSummaryMessage ( fileName ) , message . tabID , 'answer-part' )
568-
569- // Grab the selection from the fileEditorToTest and get the vscode Range
570- const selection = fileEditorToTest . selection
571- let selectionRange = undefined
572- if (
573- selection . start . line !== selection . end . line ||
574- selection . start . character !== selection . end . character
575- ) {
576- selectionRange = new vscode . Range (
577- selection . start . line ,
578- selection . start . character ,
579- selection . end . line ,
580- selection . end . character
560+ if ( ! [ 'java' , 'python' ] . includes ( language ) || workspaceFolder === undefined ) {
561+ let unsupportedMessage : string
562+ const unsupportedLanguage = language ? language . charAt ( 0 ) . toUpperCase ( ) + language . slice ( 1 ) : ''
563+ if ( ! workspaceFolder ) {
564+ // File is outside of workspace
565+ unsupportedMessage = `<span style="color: #EE9D28;">⚠<b>I can't generate tests for ${ fileName } </b> because the file is outside of workspace scope.<br></span> I can still provide examples, instructions and code suggestions.`
566+ } else if ( unsupportedLanguage ) {
567+ unsupportedMessage = `<span style="color: #EE9D28;">⚠<b>I'm sorry, but /test only supports Python and Java</b><br></span> While ${ unsupportedLanguage } is not supported, I will generate a suggestion below.`
568+ } else {
569+ unsupportedMessage = `<span style="color: #EE9D28;">⚠<b>I'm sorry, but /test only supports Python and Java</b><br></span> I will still generate a suggestion below.`
570+ }
571+ this . messenger . sendMessage ( unsupportedMessage , tabID , 'answer' )
572+ session . isSupportedLanguage = false
573+ await this . onCodeGeneration (
574+ session ,
575+ userPrompt ,
576+ tabID ,
577+ fileName ,
578+ filePath ,
579+ workspaceFolder !== undefined
581580 )
581+ } else {
582+ this . messenger . sendCapabilityCard ( { tabID } )
583+ this . messenger . sendMessage ( testGenSummaryMessage ( fileName ) , message . tabID , 'answer-part' )
584+
585+ // Grab the selection from the fileEditorToTest and get the vscode Range
586+ const selection = fileEditorToTest . selection
587+ let selectionRange = undefined
588+ if (
589+ selection . start . line !== selection . end . line ||
590+ selection . start . character !== selection . end . character
591+ ) {
592+ selectionRange = new vscode . Range (
593+ selection . start . line ,
594+ selection . start . character ,
595+ selection . end . line ,
596+ selection . end . character
597+ )
598+ }
599+ session . isCodeBlockSelected = selectionRange !== undefined
600+ session . isSupportedLanguage = true
601+
602+ /**
603+ * Zip the project
604+ * Create pre-signed URL and upload artifact to S3
605+ * send API request to startTestGeneration API
606+ * Poll from getTestGeneration API
607+ * Get Diff from exportResultArchive API
608+ */
609+ ChatSessionManager . Instance . setIsInProgress ( true )
610+ await startTestGenerationProcess ( filePath , message . prompt , tabID , true , selectionRange )
582611 }
583- session . isCodeBlockSelected = selectionRange !== undefined
584- session . isSupportedLanguage = true
585-
586- /**
587- * Zip the project
588- * Create pre-signed URL and upload artifact to S3
589- * send API request to startTestGeneration API
590- * Poll from getTestGeneration API
591- * Get Diff from exportResultArchive API
592- */
593- ChatSessionManager . Instance . setIsInProgress ( true )
594- await startTestGenerationProcess ( filePath , message . prompt , tabID , true , selectionRange )
595612 }
596613 } catch ( err : any ) {
597614 // TODO: refactor error handling to be more robust
@@ -639,6 +656,32 @@ export class TestController {
639656 }
640657 }
641658
659+ /**
660+ * Process a user message using Q CLI streaming functionality
661+ */
662+ private async processQCliStreamingMessage ( data : { prompt : string ; tabID : string } ) : Promise < void > {
663+ const session = this . sessionStorage . getSession ( )
664+ const logger = getLogger ( )
665+
666+ try {
667+ logger . debug ( 'Processing Q CLI streaming message: %s' , data . prompt )
668+
669+ // // Check authentication
670+ // const authState = await AuthUtil.instance.getChatAuthState()
671+ // if (authState.amazonQ !== 'connected') {
672+ // void this.messenger.sendAuthNeededExceptionMessage(authState, data.tabID)
673+ // session.isAuthenticating = true
674+ // return
675+ // }
676+
677+ // Process the user prompt through the streaming service
678+ await QCliStreamingService . getInstance ( ) . processUserPrompt ( data . prompt , data . tabID , session )
679+ } catch ( error ) {
680+ logger . error ( 'Error processing Q CLI streaming message: %O' , error )
681+ this . messenger . sendErrorMessage ( 'Failed to process your request. Please try again.' , data . tabID )
682+ }
683+ }
684+
642685 private async updateTargetFileInfo ( message : {
643686 tabID : string
644687 targetFileInfo ?: TargetFileInfo
@@ -1410,7 +1453,10 @@ export class TestController {
14101453 if ( session . tabID ) {
14111454 getLogger ( ) . debug ( 'Setting input state with tabID: %s' , session . tabID )
14121455 this . messenger . sendChatInputEnabled ( session . tabID , true )
1413- this . messenger . sendUpdatePlaceholder ( session . tabID , 'Enter "/" for quick actions' )
1456+ this . messenger . sendUpdatePlaceholder (
1457+ session . tabID ,
1458+ 'Ask Amazon Q a question or use /test to generate unit tests...'
1459+ )
14141460 }
14151461 getLogger ( ) . debug (
14161462 'Deleting output.log and temp result directory. testGenerationLogsDir: %s' ,
0 commit comments