diff --git a/.gitignore b/.gitignore index 0a9e46bc888..5911d305a05 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ src.gen/* **/src/shared/telemetry/clienttelemetry.d.ts **/src/codewhisperer/client/codewhispererclient.d.ts **/src/codewhisperer/client/codewhispereruserclient.d.ts -**/src/amazonqFeatureDev/client/featuredevproxyclient.d.ts **/src/auth/sso/oidcclientpkce.d.ts # Generated by tests diff --git a/package-lock.json b/package-lock.json index 832dea09e5c..fc3d52b78bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17556,16 +17556,6 @@ "@types/responselike": "*" } }, - "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*" - } - }, "node_modules/@types/circular-dependency-plugin": { "version": "5.0.8", "dev": true, @@ -17600,13 +17590,6 @@ "@types/node": "*" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/diff": { "version": "5.0.7", "dev": true, @@ -17844,8 +17827,7 @@ }, "node_modules/@types/selenium-webdriver": { "version": "4.1.28", - "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.1.28.tgz", - "integrity": "sha512-Au7CXegiS7oapbB16zxPToY4Cjzi9UQQMf3W2ZZM8PigMLTGR3iUAHjPUTddyE5g1SBjT/qpmvlsAQLBfNAdKg==", + "license": "MIT", "dependencies": { "@types/node": "*", "@types/ws": "*" @@ -31624,9 +31606,6 @@ "name": "amazon-q-vscode", "version": "1.86.0-SNAPSHOT", "license": "Apache-2.0", - "devDependencies": { - "@types/chai": "^5.2.2" - }, "engines": { "npm": "^10.1.0", "vscode": "^1.83.0" diff --git a/packages/amazonq/.changes/next-release/Bug Fix-7261a487-e80a-440f-b311-2688e256a886.json b/packages/amazonq/.changes/next-release/Bug Fix-7261a487-e80a-440f-b311-2688e256a886.json new file mode 100644 index 00000000000..29d129cc287 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-7261a487-e80a-440f-b311-2688e256a886.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Faster and more responsive inline completion UX" +} diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index ae8e19a583d..8ed6b465e49 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -521,22 +521,17 @@ "command": "aws.amazonq.walkthrough.show", "group": "1_help@1" }, - { - "command": "aws.amazonq.exploreAgents", - "when": "!aws.isSageMaker", - "group": "1_help@2" - }, { "command": "aws.amazonq.github", - "group": "1_help@3" + "group": "1_help@2" }, { "command": "aws.amazonq.aboutExtension", - "group": "1_help@4" + "group": "1_help@3" }, { "command": "aws.amazonq.viewLogs", - "group": "1_help@5" + "group": "1_help@4" } ], "aws.amazonq.submenu.securityIssueMoreActions": [ @@ -843,12 +838,6 @@ "title": "%AWS.amazonq.openChat%", "category": "%AWS.amazonq.title%" }, - { - "command": "aws.amazonq.exploreAgents", - "title": "%AWS.amazonq.exploreAgents%", - "category": "%AWS.amazonq.title%", - "enablement": "aws.codewhisperer.connected && !aws.isSageMaker" - }, { "command": "aws.amazonq.walkthrough.show", "title": "%AWS.amazonq.welcomeWalkthrough%" @@ -972,6 +961,10 @@ "command": "aws.amazonq.showPrev", "when": "inlineSuggestionVisible && !editorReadonly && aws.codewhisperer.connected" }, + { + "command": "aws.amazonq.checkInlineSuggestionVisibility", + "when": "inlineSuggestionVisible && !editorReadonly && aws.codewhisperer.connected" + }, { "command": "aws.amazonq.inline.invokeChat", "win": "ctrl+i", diff --git a/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts index 178045afaee..45a615e318e 100644 --- a/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts +++ b/packages/amazonq/src/app/inline/EditRendering/svgGenerator.ts @@ -30,7 +30,7 @@ export class SvgGenerationService { origionalCodeHighlightRange: Range[] }> { const textDoc = await vscode.workspace.openTextDocument(filePath) - const originalCode = textDoc.getText() + const originalCode = textDoc.getText().replaceAll('\r\n', '\n') if (originalCode === '') { logger.error(`udiff format error`) throw new ToolkitError('udiff format error') diff --git a/packages/amazonq/src/app/inline/completion.ts b/packages/amazonq/src/app/inline/completion.ts index be49a0654cc..66668be1849 100644 --- a/packages/amazonq/src/app/inline/completion.ts +++ b/packages/amazonq/src/app/inline/completion.ts @@ -2,7 +2,7 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - +import * as vscode from 'vscode' import { CancellationToken, InlineCompletionContext, @@ -32,7 +32,6 @@ import { ImportAdderProvider, CodeSuggestionsState, vsCodeState, - inlineCompletionsDebounceDelay, noInlineSuggestionsMsg, getDiagnosticsDifferences, getDiagnosticsOfCurrentFile, @@ -42,7 +41,7 @@ import { LineTracker } from './stateTracker/lineTracker' import { InlineTutorialAnnotation } from './tutorials/inlineTutorialAnnotation' import { TelemetryHelper } from './telemetryHelper' import { Experiments, getLogger, sleep } from 'aws-core-vscode/shared' -import { debounce, messageUtils } from 'aws-core-vscode/utils' +import { messageUtils } from 'aws-core-vscode/utils' import { showEdits } from './EditRendering/imageRenderer' import { ICursorUpdateRecorder } from './cursorUpdateManager' import { DocumentEventListener } from './documentEventListener' @@ -164,6 +163,11 @@ export class InlineCompletionManager implements Disposable { const onInlineRejection = async () => { try { vsCodeState.isCodeWhispererEditing = true + if (this.sessionManager.getActiveSession() === undefined) { + return + } + const requestStartTime = this.sessionManager.getActiveSession()!.requestStartTime + const totalSessionDisplayTime = performance.now() - requestStartTime await commands.executeCommand('editor.action.inlineSuggest.hide') // TODO: also log the seen state for other suggestions in session this.disposable.dispose() @@ -185,6 +189,7 @@ export class InlineCompletionManager implements Disposable { discarded: false, }, }, + totalSessionDisplayTime: totalSessionDisplayTime, } this.languageClient.sendNotification(this.logSessionResultMessageName, params) // clear session manager states once rejected @@ -198,7 +203,7 @@ export class InlineCompletionManager implements Disposable { } export class AmazonQInlineCompletionItemProvider implements InlineCompletionItemProvider { - private logger = getLogger('nextEditPrediction') + private logger = getLogger() constructor( private readonly languageClient: LanguageClient, private readonly recommendationService: RecommendationService, @@ -208,13 +213,23 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem ) {} private readonly logSessionResultMessageName = 'aws/logInlineCompletionSessionResults' - provideInlineCompletionItems = debounce( - this._provideInlineCompletionItems.bind(this), - inlineCompletionsDebounceDelay, - true - ) - private async _provideInlineCompletionItems( + // Ideally use this API handleDidShowCompletionItem + // https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts#L83 + // we need this because the returned items of provideInlineCompletionItems may not be actually rendered on screen + // if VS Code believes the user is actively typing then it will not show such item + async checkWhetherInlineCompletionWasShown() { + // this line is to force VS Code to re-render the inline completion + // if it decides the inline completion can be shown + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') + // yield event loop to let backend state transition finish plus wait for vsc to render + await sleep(10) + // run the command to detect if inline suggestion is really shown or not + await vscode.commands.executeCommand(`aws.amazonq.checkInlineSuggestionVisibility`) + } + + // this method is automatically invoked by VS Code as user types + async provideInlineCompletionItems( document: TextDocument, position: Position, context: InlineCompletionContext, @@ -299,26 +314,28 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem } // re-use previous suggestions as long as new typed prefix matches if (prevItemMatchingPrefix.length > 0) { - getLogger().debug(`Re-using suggestions that match user typed characters`) + logstr += `- not call LSP and reuse previous suggestions that match user typed characters + - duration between trigger to completion suggestion is displayed ${performance.now() - t0}` + void this.checkWhetherInlineCompletionWasShown() return prevItemMatchingPrefix } - getLogger().debug(`Auto rejecting suggestions from previous session`) - // if no such suggestions, report the previous suggestion as Reject + + // if no such suggestions, report the previous suggestion as Reject or Discarded const params: LogInlineCompletionSessionResultsParams = { sessionId: prevSessionId, completionSessionResult: { [prevItemId]: { - seen: true, + seen: prevSession.displayed, accepted: false, - discarded: false, + discarded: !prevSession.displayed, }, }, + totalSessionDisplayTime: performance.now() - prevSession.requestStartTime, } this.languageClient.sendNotification(this.logSessionResultMessageName, params) this.sessionManager.clear() } - // TODO: this line will take ~200ms each trigger, need to root cause and maybe better to disable it for now // tell the tutorial that completions has been triggered await this.inlineTutorialAnnotation.triggered(context.triggerKind) @@ -346,12 +363,13 @@ export class AmazonQInlineCompletionItemProvider implements InlineCompletionItem const t2 = performance.now() - logstr = logstr += `- number of suggestions: ${items.length} + logstr += `- number of suggestions: ${items.length} - sessionId: ${this.sessionManager.getActiveSession()?.sessionId} - first suggestion content (next line): ${itemLog} -- duration since trigger to before sending Flare call: ${t1 - t0}ms -- duration since trigger to receiving responses from Flare: ${t2 - t0}ms +- duration between trigger to before sending LSP call: ${t1 - t0}ms +- duration between trigger to after receiving LSP response: ${t2 - t0}ms +- duration between before sending LSP call to after receving LSP response: ${t2 - t1}ms ` const session = this.sessionManager.getActiveSession() @@ -361,16 +379,13 @@ ${itemLog} } if (!session || !items.length || !editor) { - getLogger().debug( - `Failed to produce inline suggestion results. Received ${items.length} items from service` - ) + logstr += `Failed to produce inline suggestion results. Received ${items.length} items from service` return [] } const cursorPosition = document.validatePosition(position) if (position.isAfter(editor.selection.active)) { - getLogger().debug(`Cursor moved behind trigger position. Discarding suggestion...`) const params: LogInlineCompletionSessionResultsParams = { sessionId: session.sessionId, completionSessionResult: { @@ -383,6 +398,7 @@ ${itemLog} } this.languageClient.sendNotification(this.logSessionResultMessageName, params) this.sessionManager.clear() + logstr += `- cursor moved behind trigger position. Discarding suggestion...` return [] } @@ -410,9 +426,7 @@ ${itemLog} // Check if Next Edit Prediction feature flag is enabled if (Experiments.instance.get('amazonqLSPNEP', true)) { await showEdits(item, editor, session, this.languageClient, this) - const t3 = performance.now() - logstr = logstr + `- duration since trigger to NEP suggestion is displayed: ${t3 - t0}ms` - this.logger.info(logstr) + logstr += `- duration between trigger to edits suggestion is displayed: ${performance.now() - t0}ms` } return [] } @@ -438,9 +452,6 @@ ${itemLog} // report discard if none of suggestions match typeahead if (itemsMatchingTypeahead.length === 0) { - getLogger().debug( - `Suggestion does not match user typeahead from insertion position. Discarding suggestion...` - ) const params: LogInlineCompletionSessionResultsParams = { sessionId: session.sessionId, completionSessionResult: { @@ -453,17 +464,22 @@ ${itemLog} } this.languageClient.sendNotification(this.logSessionResultMessageName, params) this.sessionManager.clear() + logstr += `- suggestion does not match user typeahead from insertion position. Discarding suggestion...` return [] } this.sessionManager.updateCodeReferenceAndImports() // suggestions returned here will be displayed on screen + logstr += `- duration between trigger to completion suggestion is displayed: ${performance.now() - t0}ms` + void this.checkWhetherInlineCompletionWasShown() return itemsMatchingTypeahead as InlineCompletionItem[] } catch (e) { getLogger('amazonqLsp').error('Failed to provide completion items: %O', e) + logstr += `- failed to provide completion items ${(e as Error).message}` return [] } finally { vsCodeState.isRecommendationsActive = false + this.logger.info(logstr) } } } diff --git a/packages/amazonq/src/app/inline/recommendationService.ts b/packages/amazonq/src/app/inline/recommendationService.ts index 1329c68a51c..794d6c46183 100644 --- a/packages/amazonq/src/app/inline/recommendationService.ts +++ b/packages/amazonq/src/app/inline/recommendationService.ts @@ -12,10 +12,10 @@ import { import { CancellationToken, InlineCompletionContext, Position, TextDocument } from 'vscode' import { LanguageClient } from 'vscode-languageclient' import { SessionManager } from './sessionManager' -import { AuthUtil, CodeWhispererStatusBarManager } from 'aws-core-vscode/codewhisperer' +import { AuthUtil, CodeWhispererStatusBarManager, vsCodeState } from 'aws-core-vscode/codewhisperer' import { TelemetryHelper } from './telemetryHelper' import { ICursorUpdateRecorder } from './cursorUpdateManager' -import { globals, getLogger } from 'aws-core-vscode/shared' +import { getLogger } from 'aws-core-vscode/shared' export interface GetAllRecommendationsOptions { emitTelemetry?: boolean @@ -68,7 +68,7 @@ export class RecommendationService { if (options.editsStreakToken) { request = { ...request, partialResultToken: options.editsStreakToken } } - const requestStartTime = globals.clock.Date.now() + const requestStartTime = performance.now() const statusBar = CodeWhispererStatusBarManager.instance // Only track telemetry if enabled @@ -92,13 +92,15 @@ export class RecommendationService { nextToken: request.partialResultToken, }, }) + const t0 = performance.now() const result: InlineCompletionListWithReferences = await languageClient.sendRequest( inlineCompletionWithReferencesRequestType.method, request, token ) - getLogger().info('Received inline completion response: %O', { + getLogger().info('Received inline completion response from LSP: %O', { sessionId: result.sessionId, + latency: performance.now() - t0, itemCount: result.items?.length || 0, items: result.items?.map((item) => ({ itemId: item.itemId, @@ -117,7 +119,7 @@ export class RecommendationService { } TelemetryHelper.instance.setFirstSuggestionShowTime() - const firstCompletionDisplayLatency = globals.clock.Date.now() - requestStartTime + const firstCompletionDisplayLatency = performance.now() - requestStartTime this.sessionManager.startSession( result.sessionId, result.items, @@ -128,6 +130,7 @@ export class RecommendationService { const isInlineEdit = result.items.some((item) => item.isInlineEdit) + // TODO: question, is it possible that the first request returns empty suggestion but has non-empty next token? if (result.partialResultToken) { if (!isInlineEdit) { // If the suggestion is COMPLETIONS and there are more results to fetch, handle them in the background @@ -183,6 +186,11 @@ export class RecommendationService { request, token ) + // when pagination is in progress, but user has already accepted or rejected an inline completion + // then stop pagination + if (this.sessionManager.getActiveSession() === undefined || vsCodeState.isCodeWhispererEditing) { + break + } this.sessionManager.updateSessionSuggestions(result.items) nextToken = result.partialResultToken } diff --git a/packages/amazonq/src/app/inline/sessionManager.ts b/packages/amazonq/src/app/inline/sessionManager.ts index eaa6eaa23b9..7decf035b9a 100644 --- a/packages/amazonq/src/app/inline/sessionManager.ts +++ b/packages/amazonq/src/app/inline/sessionManager.ts @@ -24,6 +24,8 @@ export interface CodeWhispererSession { // partialResultToken for the next trigger if user accepts an EDITS suggestion editsStreakPartialResultToken?: number | string triggerOnAcceptance?: boolean + // whether any suggestion in this session was displayed on screen + displayed: boolean } export class SessionManager { @@ -49,6 +51,7 @@ export class SessionManager { startPosition, firstCompletionDisplayLatency, diagnosticsBeforeAccept, + displayed: false, } this._currentSuggestionIndex = 0 } @@ -128,6 +131,12 @@ export class SessionManager { } } + public checkInlineSuggestionVisibility() { + if (this.activeSession) { + this.activeSession.displayed = true + } + } + private clearReferenceInlineHintsAndImportHints() { ReferenceInlineProvider.instance.removeInlineReference() ImportAdderProvider.instance.clear() diff --git a/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts index bd12b1d28dd..ad0807df94c 100644 --- a/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts +++ b/packages/amazonq/src/app/inline/tutorials/inlineTutorialAnnotation.ts @@ -5,13 +5,7 @@ import * as vscode from 'vscode' import * as os from 'os' -import { - AnnotationChangeSource, - AuthUtil, - inlinehintKey, - runtimeLanguageContext, - TelemetryHelper, -} from 'aws-core-vscode/codewhisperer' +import { AnnotationChangeSource, AuthUtil, inlinehintKey, runtimeLanguageContext } from 'aws-core-vscode/codewhisperer' import { editorUtilities, getLogger, globals, setContext, vscodeUtilities } from 'aws-core-vscode/shared' import { LinesChangeEvent, LineSelection, LineTracker } from '../stateTracker/lineTracker' import { telemetry } from 'aws-core-vscode/telemetry' @@ -296,28 +290,27 @@ export class InlineTutorialAnnotation implements vscode.Disposable { } async triggered(triggerType: vscode.InlineCompletionTriggerKind): Promise { - await telemetry.withTraceId(async () => { - if (!this._isReady) { - return - } - - if (this._currentState instanceof ManualtriggerState) { - if ( - triggerType === vscode.InlineCompletionTriggerKind.Invoke && - this._currentState.hasManualTrigger === false - ) { - this._currentState.hasManualTrigger = true - } - if ( - this.sessionManager.getActiveRecommendation().length > 0 && - this._currentState.hasValidResponse === false - ) { - this._currentState.hasValidResponse = true - } - } - - await this.refresh(vscode.window.activeTextEditor, 'codewhisperer') - }, TelemetryHelper.instance.traceId) + // TODO: this logic will take ~200ms each trigger, need to root cause and re-enable once it's fixed, or it should only be invoked when the tutorial is actually needed + // await telemetry.withTraceId(async () => { + // if (!this._isReady) { + // return + // } + // if (this._currentState instanceof ManualtriggerState) { + // if ( + // triggerType === vscode.InlineCompletionTriggerKind.Invoke && + // this._currentState.hasManualTrigger === false + // ) { + // this._currentState.hasManualTrigger = true + // } + // if ( + // this.sessionManager.getActiveRecommendation().length > 0 && + // this._currentState.hasValidResponse === false + // ) { + // this._currentState.hasValidResponse = true + // } + // } + // await this.refresh(vscode.window.activeTextEditor, 'codewhisperer') + // }, TelemetryHelper.instance.traceId) } isTutorialDone(): boolean { diff --git a/packages/amazonq/src/lsp/chat/messages.ts b/packages/amazonq/src/lsp/chat/messages.ts index 7b3b130ff85..a95b99b442c 100644 --- a/packages/amazonq/src/lsp/chat/messages.ts +++ b/packages/amazonq/src/lsp/chat/messages.ts @@ -97,7 +97,7 @@ import { ViewDiffMessage, referenceLogText, } from 'aws-core-vscode/amazonq' -import { telemetry, TelemetryBase } from 'aws-core-vscode/telemetry' +import { telemetry } from 'aws-core-vscode/telemetry' import { isValidResponseError } from './error' import { decryptResponse, encryptRequest } from '../encryption' import { getCursorState } from '../utils' @@ -144,10 +144,13 @@ export function registerLanguageServerEventListener(languageClient: LanguageClie // This passes through metric data from LSP events to Toolkit telemetry with all fields from the LSP server languageClient.onTelemetry((e) => { const telemetryName: string = e.name - - if (telemetryName in telemetry) { - languageClient.info(`[VSCode Telemetry] Emitting ${telemetryName} telemetry: ${JSON.stringify(e.data)}`) - telemetry[telemetryName as keyof TelemetryBase].emit(e.data) + languageClient.info(`[VSCode Telemetry] Emitting ${telemetryName} telemetry: ${JSON.stringify(e.data)}`) + try { + // Flare is now the source of truth for metrics instead of depending on each IDE client and toolkit-common + const metric = (telemetry as any).getMetric(telemetryName) + metric?.emit(e.data) + } catch (error) { + languageClient.warn(`[VSCode Telemetry] Failed to emit ${telemetryName}: ${error}`) } }) } diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 4d052912c8e..d335dae40ef 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -39,6 +39,7 @@ import { getClientId, extensionVersion, isSageMaker, + DevSettings, } from 'aws-core-vscode/shared' import { processUtils } from 'aws-core-vscode/shared' import { activate } from './chat/activation' @@ -129,6 +130,15 @@ export async function startLanguageServer( await validateNodeExe(executable, resourcePaths.lsp, argv, logger) + const endpointOverride = DevSettings.instance.get('codewhispererService', {}).endpoint ?? undefined + const textDocSection = { + inlineEditSupport: Experiments.instance.get('amazonqLSPNEP', true), + } as any + + if (endpointOverride) { + textDocSection.endpointOverride = endpointOverride + } + // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for json documents @@ -174,12 +184,10 @@ export async function startLanguageServer( window: { notifications: true, showSaveFileDialog: true, - showLogs: true, + showLogs: isSageMaker() ? false : true, }, textDocument: { - inlineCompletionWithReferences: { - inlineEditSupport: Experiments.instance.get('amazonqLSPNEP', true), - }, + inlineCompletionWithReferences: textDocSection, }, }, contextConfiguration: { @@ -352,6 +360,10 @@ async function onLanguageServerReady( await vscode.commands.executeCommand('editor.action.inlineSuggest.showNext') sessionManager.onNextSuggestion() }), + // this is a workaround since handleDidShowCompletionItem is not public API + Commands.register('aws.amazonq.checkInlineSuggestionVisibility', async () => { + sessionManager.checkInlineSuggestionVisibility() + }), Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') }), diff --git a/packages/amazonq/test/e2e/amazonq/explore.test.ts b/packages/amazonq/test/e2e/amazonq/explore.test.ts deleted file mode 100644 index 970d93d00bb..00000000000 --- a/packages/amazonq/test/e2e/amazonq/explore.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'assert' -import sinon from 'sinon' -import { qTestingFramework } from './framework/framework' -import { Messenger } from './framework/messenger' - -describe('Amazon Q Explore page', function () { - let framework: qTestingFramework - let tab: Messenger - - beforeEach(() => { - framework = new qTestingFramework('agentWalkthrough', true, [], 0) - const welcomeTab = framework.getTabs()[0] - welcomeTab.clickInBodyButton('explore') - - // Find the new explore tab - const exploreTab = framework.findTab('Explore') - if (!exploreTab) { - assert.fail('Explore tab not found') - } - tab = exploreTab - }) - - afterEach(() => { - framework.removeTab(tab.tabID) - framework.dispose() - sinon.restore() - }) - - // TODO refactor page objects so we can associate clicking user guides with actual urls - // TODO test that clicking quick start changes the tab title, etc - it('should have correct button IDs', async () => { - const features = ['featuredev', 'testgen', 'doc', 'review', 'gumby'] - - for (const [index, feature] of features.entries()) { - const buttons = (tab.getStore().chatItems ?? [])[index].buttons ?? [] - assert.deepStrictEqual(buttons[0].id, `user-guide-${feature}`) - assert.deepStrictEqual(buttons[1].id, `quick-start-${feature}`) - } - }) -}) diff --git a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts index 7b079eaad17..417c8be1426 100644 --- a/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts +++ b/packages/amazonq/test/unit/amazonq/apps/inline/completion.test.ts @@ -378,7 +378,7 @@ describe('InlineCompletionManager', () => { ) await messageShown }) - describe('debounce behavior', function () { + describe.skip('debounce behavior', function () { let clock: ReturnType beforeEach(function () { @@ -389,7 +389,7 @@ describe('InlineCompletionManager', () => { clock.uninstall() }) - it('should only trigger once on rapid events', async () => { + it.skip('should only trigger once on rapid events', async () => { provider = new AmazonQInlineCompletionItemProvider( languageClient, recommendationService, diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts deleted file mode 100644 index 4c6073114f8..00000000000 --- a/packages/amazonq/test/unit/amazonqFeatureDev/session/chatSessionStorage.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as assert from 'assert' -import { FeatureDevChatSessionStorage } from 'aws-core-vscode/amazonqFeatureDev' -import { Messenger } from 'aws-core-vscode/amazonq' -import { createMessenger } from 'aws-core-vscode/test' - -describe('chatSession', () => { - const tabID = '1234' - let chatStorage: FeatureDevChatSessionStorage - let messenger: Messenger - - beforeEach(() => { - messenger = createMessenger() - chatStorage = new FeatureDevChatSessionStorage(messenger) - }) - - it('locks getSession', async () => { - const results = await Promise.allSettled([chatStorage.getSession(tabID), chatStorage.getSession(tabID)]) - assert.equal(results.length, 2) - assert.deepStrictEqual(results[0], results[1]) - }) -}) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts deleted file mode 100644 index 39c38de555f..00000000000 --- a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as assert from 'assert' - -import sinon from 'sinon' - -import { - ControllerSetup, - createController, - createMessenger, - createSession, - generateVirtualMemoryUri, - sessionRegisterProvider, - sessionWriteFile, - assertTelemetry, -} from 'aws-core-vscode/test' -import { FeatureDevClient, featureDevScheme, FeatureDevCodeGenState } from 'aws-core-vscode/amazonqFeatureDev' -import { Messenger, CurrentWsFolders } from 'aws-core-vscode/amazonq' -import path from 'path' -import { fs } from 'aws-core-vscode/shared' - -describe('session', () => { - const conversationID = '12345' - let messenger: Messenger - - beforeEach(() => { - messenger = createMessenger() - }) - - afterEach(() => { - sinon.restore() - }) - - describe('preloader', () => { - it('emits start chat telemetry', async () => { - const session = await createSession({ messenger, conversationID, scheme: featureDevScheme }) - session.latestMessage = 'implement twosum in typescript' - - await session.preloader() - - assertTelemetry('amazonq_startConversationInvoke', { - amazonqConversationId: conversationID, - }) - }) - }) - describe('insertChanges', async () => { - afterEach(() => { - sinon.restore() - }) - - let workspaceFolderUriFsPath: string - const notRejectedFileName = 'notRejectedFile.js' - const notRejectedFileContent = 'notrejectedFileContent' - let uri: vscode.Uri - let encodedContent: Uint8Array - - async function createCodeGenState() { - const controllerSetup: ControllerSetup = await createController() - - const uploadID = '789' - const tabID = '123' - const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders - workspaceFolderUriFsPath = controllerSetup.workspaceFolder.uri.fsPath - uri = generateVirtualMemoryUri(uploadID, notRejectedFileName, featureDevScheme) - - const testConfig = { - conversationId: conversationID, - proxyClient: {} as unknown as FeatureDevClient, - workspaceRoots: [''], - uploadId: uploadID, - workspaceFolders, - } - - const codeGenState = new FeatureDevCodeGenState( - testConfig, - [ - { - zipFilePath: notRejectedFileName, - relativePath: notRejectedFileName, - fileContent: notRejectedFileContent, - rejected: false, - virtualMemoryUri: uri, - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - { - zipFilePath: 'rejectedFile.js', - relativePath: 'rejectedFile.js', - fileContent: 'rejectedFileContent', - rejected: true, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'rejectedFile.js', featureDevScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ], - [], - [], - tabID, - 0, - {} - ) - const session = await createSession({ - messenger, - sessionState: codeGenState, - conversationID, - scheme: featureDevScheme, - }) - encodedContent = new TextEncoder().encode(notRejectedFileContent) - await sessionRegisterProvider(session, uri, encodedContent) - return session - } - it('only insert non rejected files', async () => { - const fsSpyWriteFile = sinon.spy(fs, 'writeFile') - const session = await createCodeGenState() - sinon.stub(session, 'sendLinesOfCodeAcceptedTelemetry').resolves() - await sessionWriteFile(session, uri, encodedContent) - await session.insertChanges() - - const absolutePath = path.join(workspaceFolderUriFsPath, notRejectedFileName) - - assert.ok(fsSpyWriteFile.calledOnce) - assert.ok(fsSpyWriteFile.calledWith(absolutePath, notRejectedFileContent)) - }) - }) -}) diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts deleted file mode 100644 index 574d0a25a19..00000000000 --- a/packages/amazonq/test/unit/amazonqFeatureDev/util/files.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import assert from 'assert' -import { - prepareRepoData, - PrepareRepoDataOptions, - TelemetryHelper, - maxRepoSizeBytes, -} from 'aws-core-vscode/amazonqFeatureDev' -import { assertTelemetry, getWorkspaceFolder, TestFolder } from 'aws-core-vscode/test' -import { fs, AmazonqCreateUpload, ZipStream, ContentLengthError } from 'aws-core-vscode/shared' -import { MetricName, Span } from 'aws-core-vscode/telemetry' -import sinon from 'sinon' -import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' -import { CurrentWsFolders } from 'aws-core-vscode/amazonq' -import path from 'path' - -const testDevfilePrepareRepo = async (devfileEnabled: boolean) => { - const files: Record = { - 'file.md': 'test content', - // only include when execution is enabled - 'devfile.yaml': 'test', - // .git folder is always dropped (because of vscode global exclude rules) - '.git/ref': '####', - // .gitignore should always be included - '.gitignore': 'node_models/*', - // non code files only when dev execution is enabled - 'abc.jar': 'jar-content', - 'data/logo.ico': 'binary-content', - } - const folder = await TestFolder.create() - - for (const [fileName, content] of Object.entries(files)) { - await folder.write(fileName, content) - } - - const expectedFiles = !devfileEnabled - ? ['file.md', '.gitignore'] - : ['devfile.yaml', 'file.md', '.gitignore', 'abc.jar', 'data/logo.ico'] - - const workspace = getWorkspaceFolder(folder.path) - sinon - .stub(CodeWhispererSettings.instance, 'getAutoBuildSetting') - .returns(devfileEnabled ? { [workspace.uri.fsPath]: true } : {}) - - await testPrepareRepoData([workspace], expectedFiles, { telemetry: new TelemetryHelper() }) -} - -const testPrepareRepoData = async ( - workspaces: vscode.WorkspaceFolder[], - expectedFiles: string[], - prepareRepoDataOptions: PrepareRepoDataOptions, - expectedTelemetryMetrics?: Array<{ metricName: MetricName; value: any }> -) => { - expectedFiles.sort((a, b) => a.localeCompare(b)) - const result = await prepareRepoData( - workspaces.map((ws) => ws.uri.fsPath), - workspaces as CurrentWsFolders, - { - record: () => {}, - } as unknown as Span, - prepareRepoDataOptions - ) - - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - // checksum is not the same across different test executions because some unique random folder names are generated - assert.strictEqual(result.zipFileChecksum.length, 44) - - if (expectedTelemetryMetrics) { - for (const metric of expectedTelemetryMetrics) { - assertTelemetry(metric.metricName, metric.value) - } - } - - // Unzip the buffer and compare the entry names - const zipEntries = await ZipStream.unzip(result.zipFileBuffer) - const actualZipEntries = zipEntries.map((entry) => entry.filename) - actualZipEntries.sort((a, b) => a.localeCompare(b)) - assert.deepStrictEqual(actualZipEntries, expectedFiles) -} - -describe('file utils', () => { - describe('prepareRepoData', function () { - const defaultPrepareRepoDataOptions: PrepareRepoDataOptions = { telemetry: new TelemetryHelper() } - - afterEach(() => { - sinon.restore() - }) - - it('returns files in the workspace as a zip', async function () { - const folder = await TestFolder.create() - await folder.write('file1.md', 'test content') - await folder.write('file2.md', 'test content') - await folder.write('docs/infra.svg', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - await testPrepareRepoData([workspace], ['file1.md', 'file2.md'], defaultPrepareRepoDataOptions) - }) - - it('infrastructure diagram is included', async function () { - const folder = await TestFolder.create() - await folder.write('file1.md', 'test content') - await folder.write('file2.svg', 'test content') - await folder.write('docs/infra.svg', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - await testPrepareRepoData([workspace], ['file1.md', 'docs/infra.svg'], { - telemetry: new TelemetryHelper(), - isIncludeInfraDiagram: true, - }) - }) - - it('prepareRepoData ignores denied file extensions', async function () { - const folder = await TestFolder.create() - await folder.write('file.mp4', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - await testPrepareRepoData([workspace], [], defaultPrepareRepoDataOptions, [ - { metricName: 'amazonq_bundleExtensionIgnored', value: { filenameExt: 'mp4', count: 1 } }, - ]) - }) - - it('should ignore devfile.yaml when setting is disabled', async function () { - await testDevfilePrepareRepo(false) - }) - - it('should include devfile.yaml when setting is enabled', async function () { - await testDevfilePrepareRepo(true) - }) - - // Test the logic that allows the customer to modify root source folder - it('prepareRepoData throws a ContentLengthError code when repo is too big', async function () { - const folder = await TestFolder.create() - await folder.write('file.md', 'test content') - const workspace = getWorkspaceFolder(folder.path) - - sinon.stub(fs, 'stat').resolves({ size: 2 * maxRepoSizeBytes } as vscode.FileStat) - await assert.rejects( - () => - prepareRepoData( - [workspace.uri.fsPath], - [workspace], - { - record: () => {}, - } as unknown as Span, - defaultPrepareRepoDataOptions - ), - ContentLengthError - ) - }) - - it('prepareRepoData properly handles multi-root workspaces', async function () { - const folder = await TestFolder.create() - const testFilePath = 'innerFolder/file.md' - await folder.write(testFilePath, 'test content') - - // Add a folder and its subfolder to the workspace - const workspace1 = getWorkspaceFolder(folder.path) - const workspace2 = getWorkspaceFolder(folder.path + '/innerFolder') - const folderName = path.basename(folder.path) - - await testPrepareRepoData( - [workspace1, workspace2], - [`${folderName}_${workspace1.name}/${testFilePath}`], - defaultPrepareRepoDataOptions - ) - }) - }) -}) diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 0a25550ec22..498a3583a00 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -354,7 +354,6 @@ "AWS.amazonq.security": "Code Issues", "AWS.amazonq.login": "Login", "AWS.amazonq.learnMore": "Learn More About Amazon Q", - "AWS.amazonq.exploreAgents": "Explore Agent Capabilities", "AWS.amazonq.welcomeWalkthrough": "Welcome Walkthrough", "AWS.amazonq.codewhisperer.title": "Amazon Q", "AWS.amazonq.toggleCodeSuggestion": "Toggle Auto-Suggestions", diff --git a/packages/core/scripts/build/generateServiceClient.ts b/packages/core/scripts/build/generateServiceClient.ts index 7ef217be21b..5d1854527b9 100644 --- a/packages/core/scripts/build/generateServiceClient.ts +++ b/packages/core/scripts/build/generateServiceClient.ts @@ -241,10 +241,6 @@ void (async () => { serviceJsonPath: 'src/codewhisperer/client/user-service-2.json', serviceName: 'CodeWhispererUserClient', }, - { - serviceJsonPath: 'src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json', - serviceName: 'FeatureDevProxyClient', - }, ] await generateServiceClients(serviceClientDefinitions) })() diff --git a/packages/core/src/amazonq/commons/connector/baseMessenger.ts b/packages/core/src/amazonq/commons/connector/baseMessenger.ts deleted file mode 100644 index c26834c6fff..00000000000 --- a/packages/core/src/amazonq/commons/connector/baseMessenger.ts +++ /dev/null @@ -1,219 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemAction, ProgressField } from '@aws/mynah-ui' -import { AuthFollowUpType, AuthMessageDataMap } from '../../../amazonq/auth/model' -import { i18n } from '../../../shared/i18n-helper' -import { CodeReference } from '../../../amazonq/webview/ui/connector' - -import { MessengerTypes } from '../../../amazonqFeatureDev/controllers/chat/messenger/constants' -import { - AppToWebViewMessageDispatcher, - AsyncEventProgressMessage, - AuthenticationUpdateMessage, - AuthNeededException, - ChatInputEnabledMessage, - ChatMessage, - CodeResultMessage, - FileComponent, - FolderConfirmationMessage, - OpenNewTabMessage, - UpdateAnswerMessage, - UpdatePlaceholderMessage, - UpdatePromptProgressMessage, -} from './connectorMessages' -import { DeletedFileInfo, FollowUpTypes, NewFileInfo } from '../types' -import { messageWithConversationId } from '../../../amazonqFeatureDev/userFacingText' -import { FeatureAuthState } from '../../../codewhisperer/util/authUtil' - -export class Messenger { - public constructor( - private readonly dispatcher: AppToWebViewMessageDispatcher, - private readonly sender: string - ) {} - - public sendAnswer(params: { - message?: string - type: MessengerTypes - followUps?: ChatItemAction[] - tabID: string - canBeVoted?: boolean - snapToTop?: boolean - messageId?: string - disableChatInput?: boolean - }) { - this.dispatcher.sendChatMessage( - new ChatMessage( - { - message: params.message, - messageType: params.type, - followUps: params.followUps, - relatedSuggestions: undefined, - canBeVoted: params.canBeVoted ?? false, - snapToTop: params.snapToTop ?? false, - messageId: params.messageId, - }, - params.tabID, - this.sender - ) - ) - if (params.disableChatInput) { - this.sendChatInputEnabled(params.tabID, false) - } - } - - public sendFeedback(tabID: string) { - this.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.sendFeedback'), - type: FollowUpTypes.SendFeedback, - status: 'info', - }, - ], - tabID, - }) - } - - public sendMonthlyLimitError(tabID: string) { - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), - disableChatInput: true, - }) - this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.chatInputDisabled')) - } - - public sendUpdatePromptProgress(tabID: string, progressField: ProgressField | null) { - this.dispatcher.sendUpdatePromptProgress(new UpdatePromptProgressMessage(tabID, this.sender, progressField)) - } - - public sendFolderConfirmationMessage( - tabID: string, - message: string, - folderPath: string, - followUps?: ChatItemAction[] - ) { - this.dispatcher.sendFolderConfirmationMessage( - new FolderConfirmationMessage(tabID, this.sender, message, folderPath, followUps) - ) - - this.sendChatInputEnabled(tabID, false) - } - - public sendErrorMessage( - errorMessage: string, - tabID: string, - retries: number, - conversationId?: string, - showDefaultMessage?: boolean - ) { - if (retries === 0) { - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: showDefaultMessage ? errorMessage : i18n('AWS.amazonq.featureDev.error.technicalDifficulties'), - canBeVoted: true, - }) - this.sendFeedback(tabID) - return - } - - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: errorMessage + messageWithConversationId(conversationId), - }) - - this.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ], - tabID, - }) - } - - public sendCodeResult( - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - uploadId: string, - codeGenerationId: string - ) { - this.dispatcher.sendCodeResult( - new CodeResultMessage(filePaths, deletedFiles, references, tabID, this.sender, uploadId, codeGenerationId) - ) - } - - public sendAsyncEventProgress(tabID: string, inProgress: boolean, message: string | undefined) { - this.dispatcher.sendAsyncEventProgress(new AsyncEventProgressMessage(tabID, this.sender, inProgress, message)) - } - - public updateFileComponent( - tabID: string, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - messageId: string, - disableFileActions: boolean - ) { - this.dispatcher.updateFileComponent( - new FileComponent(tabID, this.sender, filePaths, deletedFiles, messageId, disableFileActions) - ) - } - - public updateChatAnswer(message: UpdateAnswerMessage) { - this.dispatcher.updateChatAnswer(message) - } - - public sendUpdatePlaceholder(tabID: string, newPlaceholder: string) { - this.dispatcher.sendPlaceholder(new UpdatePlaceholderMessage(tabID, this.sender, newPlaceholder)) - } - - public sendChatInputEnabled(tabID: string, enabled: boolean) { - this.dispatcher.sendChatInputEnabled(new ChatInputEnabledMessage(tabID, this.sender, enabled)) - } - - public sendAuthenticationUpdate(enabled: boolean, authenticatingTabIDs: string[]) { - this.dispatcher.sendAuthenticationUpdate( - new AuthenticationUpdateMessage(this.sender, enabled, authenticatingTabIDs) - ) - } - - public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string) { - let authType: AuthFollowUpType = 'full-auth' - let message = AuthMessageDataMap[authType].message - - switch (credentialState.amazonQ) { - case 'disconnected': - authType = 'full-auth' - message = AuthMessageDataMap[authType].message - break - case 'unsupported': - authType = 'use-supported-auth' - message = AuthMessageDataMap[authType].message - break - case 'expired': - authType = 're-auth' - message = AuthMessageDataMap[authType].message - break - } - - this.dispatcher.sendAuthNeededExceptionMessage(new AuthNeededException(message, authType, tabID, this.sender)) - } - - public openNewTask() { - this.dispatcher.sendOpenNewTask(new OpenNewTabMessage(this.sender)) - } -} diff --git a/packages/core/src/amazonq/commons/connector/connectorMessages.ts b/packages/core/src/amazonq/commons/connector/connectorMessages.ts deleted file mode 100644 index 6f60b786fcb..00000000000 --- a/packages/core/src/amazonq/commons/connector/connectorMessages.ts +++ /dev/null @@ -1,291 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { AuthFollowUpType } from '../../auth/model' -import { MessagePublisher } from '../../messages/messagePublisher' -import { CodeReference } from '../../webview/ui/connector' -import { ChatItemAction, ProgressField, SourceLink } from '@aws/mynah-ui' -import { ChatItemType } from '../model' -import { DeletedFileInfo, NewFileInfo } from '../types' -import { licenseText } from '../../../amazonqFeatureDev/constants' - -class UiMessage { - readonly time: number = Date.now() - readonly type: string = '' - - public constructor( - protected tabID: string, - protected sender: string - ) {} -} - -export class ErrorMessage extends UiMessage { - readonly title!: string - readonly message!: string - override type = 'errorMessage' - - constructor(title: string, message: string, tabID: string, sender: string) { - super(tabID, sender) - this.title = title - this.message = message - } -} - -export class CodeResultMessage extends UiMessage { - readonly message!: string - readonly codeGenerationId!: string - readonly references!: { - information: string - recommendationContentSpan: { - start: number - end: number - } - }[] - readonly conversationID!: string - override type = 'codeResultMessage' - - constructor( - readonly filePaths: NewFileInfo[], - readonly deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - sender: string, - conversationID: string, - codeGenerationId: string - ) { - super(tabID, sender) - this.references = references - .filter((ref) => ref.licenseName && ref.repository && ref.url) - .map((ref) => { - return { - information: licenseText(ref), - - // We're forced to provide these otherwise mynah ui errors somewhere down the line. Though they aren't used - recommendationContentSpan: { - start: 0, - end: 0, - }, - } - }) - this.codeGenerationId = codeGenerationId - this.conversationID = conversationID - } -} - -export class FolderConfirmationMessage extends UiMessage { - readonly folderPath: string - readonly message: string - readonly followUps?: ChatItemAction[] - override type = 'folderConfirmationMessage' - constructor(tabID: string, sender: string, message: string, folderPath: string, followUps?: ChatItemAction[]) { - super(tabID, sender) - this.message = message - this.folderPath = folderPath - this.followUps = followUps - } -} - -export class UpdatePromptProgressMessage extends UiMessage { - readonly progressField: ProgressField | null - override type = 'updatePromptProgress' - constructor(tabID: string, sender: string, progressField: ProgressField | null) { - super(tabID, sender) - this.progressField = progressField - } -} - -export class AsyncEventProgressMessage extends UiMessage { - readonly inProgress: boolean - readonly message: string | undefined - override type = 'asyncEventProgressMessage' - - constructor(tabID: string, sender: string, inProgress: boolean, message: string | undefined) { - super(tabID, sender) - this.inProgress = inProgress - this.message = message - } -} - -export class AuthenticationUpdateMessage { - readonly time: number = Date.now() - readonly type = 'authenticationUpdateMessage' - - constructor( - readonly sender: string, - readonly featureEnabled: boolean, - readonly authenticatingTabIDs: string[] - ) {} -} - -export class FileComponent extends UiMessage { - readonly filePaths: NewFileInfo[] - readonly deletedFiles: DeletedFileInfo[] - override type = 'updateFileComponent' - readonly messageId: string - readonly disableFileActions: boolean - - constructor( - tabID: string, - sender: string, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - messageId: string, - disableFileActions: boolean - ) { - super(tabID, sender) - this.filePaths = filePaths - this.deletedFiles = deletedFiles - this.messageId = messageId - this.disableFileActions = disableFileActions - } -} - -export class UpdatePlaceholderMessage extends UiMessage { - readonly newPlaceholder: string - override type = 'updatePlaceholderMessage' - - constructor(tabID: string, sender: string, newPlaceholder: string) { - super(tabID, sender) - this.newPlaceholder = newPlaceholder - } -} - -export class ChatInputEnabledMessage extends UiMessage { - readonly enabled: boolean - override type = 'chatInputEnabledMessage' - - constructor(tabID: string, sender: string, enabled: boolean) { - super(tabID, sender) - this.enabled = enabled - } -} - -export class OpenNewTabMessage { - readonly time: number = Date.now() - readonly type = 'openNewTabMessage' - - constructor(protected sender: string) {} -} - -export class AuthNeededException extends UiMessage { - readonly message: string - readonly authType: AuthFollowUpType - override type = 'authNeededException' - - constructor(message: string, authType: AuthFollowUpType, tabID: string, sender: string) { - super(tabID, sender) - this.message = message - this.authType = authType - } -} - -export interface ChatMessageProps { - readonly message: string | undefined - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined - readonly relatedSuggestions: SourceLink[] | undefined - readonly canBeVoted: boolean - readonly snapToTop: boolean - readonly messageId?: string -} - -export class ChatMessage extends UiMessage { - readonly message: string | undefined - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined - readonly relatedSuggestions: SourceLink[] | undefined - readonly canBeVoted: boolean - readonly requestID!: string - readonly snapToTop: boolean - readonly messageId: string | undefined - override type = 'chatMessage' - - constructor(props: ChatMessageProps, tabID: string, sender: string) { - super(tabID, sender) - this.message = props.message - this.messageType = props.messageType - this.followUps = props.followUps - this.relatedSuggestions = props.relatedSuggestions - this.canBeVoted = props.canBeVoted - this.snapToTop = props.snapToTop - this.messageId = props.messageId - } -} - -export interface UpdateAnswerMessageProps { - readonly messageId: string - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined -} - -export class UpdateAnswerMessage extends UiMessage { - readonly messageId: string - readonly messageType: ChatItemType - readonly followUps: ChatItemAction[] | undefined - override type = 'updateChatAnswer' - - constructor(props: UpdateAnswerMessageProps, tabID: string, sender: string) { - super(tabID, sender) - this.messageId = props.messageId - this.messageType = props.messageType - this.followUps = props.followUps - } -} - -export class AppToWebViewMessageDispatcher { - constructor(private readonly appsToWebViewMessagePublisher: MessagePublisher) {} - - public sendErrorMessage(message: ErrorMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatMessage(message: ChatMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendCodeResult(message: CodeResultMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendUpdatePromptProgress(message: UpdatePromptProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendFolderConfirmationMessage(message: FolderConfirmationMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAsyncEventProgress(message: AsyncEventProgressMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendPlaceholder(message: UpdatePlaceholderMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendChatInputEnabled(message: ChatInputEnabledMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthNeededExceptionMessage(message: AuthNeededException) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendAuthenticationUpdate(message: AuthenticationUpdateMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public sendOpenNewTask(message: OpenNewTabMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public updateFileComponent(message: FileComponent) { - this.appsToWebViewMessagePublisher.publish(message) - } - - public updateChatAnswer(message: UpdateAnswerMessage) { - this.appsToWebViewMessagePublisher.publish(message) - } -} diff --git a/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts b/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts deleted file mode 100644 index 4204d1d56d6..00000000000 --- a/packages/core/src/amazonq/commons/session/sessionConfigFactory.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { WorkspaceFolderNotFoundError } from '../../../amazonqFeatureDev/errors' -import { CurrentWsFolders } from '../types' -import { VirtualFileSystem } from '../../../shared/virtualFilesystem' -import { VirtualMemoryFile } from '../../../shared/virtualMemoryFile' - -export interface SessionConfig { - // The paths on disk to where the source code lives - workspaceRoots: string[] - readonly fs: VirtualFileSystem - readonly workspaceFolders: CurrentWsFolders -} - -/** - * Factory method for creating session configurations - * @returns An instantiated SessionConfig, using either the arguments provided or the defaults - */ -export async function createSessionConfig(scheme: string): Promise { - const workspaceFolders = vscode.workspace.workspaceFolders - const firstFolder = workspaceFolders?.[0] - if (workspaceFolders === undefined || workspaceFolders.length === 0 || firstFolder === undefined) { - throw new WorkspaceFolderNotFoundError() - } - - const workspaceRoots = workspaceFolders.map((f) => f.uri.fsPath) - - const fs = new VirtualFileSystem() - - // Register an empty featureDev file that's used when a new file is being added by the LLM - fs.registerProvider(vscode.Uri.from({ scheme, path: 'empty' }), new VirtualMemoryFile(new Uint8Array())) - - return Promise.resolve({ workspaceRoots, fs, workspaceFolders: [firstFolder, ...workspaceFolders.slice(1)] }) -} diff --git a/packages/core/src/amazonq/commons/types.ts b/packages/core/src/amazonq/commons/types.ts index c2d2c427596..3a4d014609d 100644 --- a/packages/core/src/amazonq/commons/types.ts +++ b/packages/core/src/amazonq/commons/types.ts @@ -4,13 +4,8 @@ */ import * as vscode from 'vscode' -import { VirtualFileSystem } from '../../shared/virtualFilesystem' -import type { CancellationTokenSource } from 'vscode' -import { CodeReference, UploadHistory } from '../webview/ui/connector' import { DiffTreeFileInfo } from '../webview/ui/diffTree/types' -import { Messenger } from './connector/baseMessenger' import { FeatureClient } from '../client/client' -import { TelemetryHelper } from '../util/telemetryHelper' import { MynahUI } from '@aws/mynah-ui' export enum FollowUpTypes { @@ -56,12 +51,6 @@ export type Interaction = { responseType?: LLMResponseType } -export interface SessionStateInteraction { - nextState: SessionState | Omit | undefined - interaction: Interaction - currentCodeGenerationId?: string -} - export enum Intent { DEV = 'DEV', DOC = 'DOC', @@ -86,24 +75,6 @@ export type SessionStatePhase = DevPhase.INIT | DevPhase.CODEGEN export type CurrentWsFolders = [vscode.WorkspaceFolder, ...vscode.WorkspaceFolder[]] -export interface SessionState { - readonly filePaths?: NewFileInfo[] - readonly deletedFiles?: DeletedFileInfo[] - readonly references?: CodeReference[] - readonly phase?: SessionStatePhase - readonly uploadId: string - readonly currentIteration?: number - currentCodeGenerationId?: string - tokenSource?: CancellationTokenSource - readonly codeGenerationId?: string - readonly tabID: string - interact(action: SessionStateAction): Promise - updateWorkspaceRoot?: (workspaceRoot: string) => void - codeGenerationRemainingIterationCount?: number - codeGenerationTotalIterationCount?: number - uploadHistory?: UploadHistory -} - export interface SessionStateConfig { workspaceRoots: string[] workspaceFolders: CurrentWsFolders @@ -113,16 +84,6 @@ export interface SessionStateConfig { currentCodeGenerationId?: string } -export interface SessionStateAction { - task: string - msg: string - messenger: Messenger - fs: VirtualFileSystem - telemetry: TelemetryHelper - uploadHistory?: UploadHistory - tokenSource?: CancellationTokenSource -} - export type NewFileZipContents = { zipFilePath: string; fileContent: string } export type NewFileInfo = DiffTreeFileInfo & NewFileZipContents & { diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 3b7737b3547..e06b8ad53d9 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -36,7 +36,6 @@ export { ChatItemType, referenceLogText } from './commons/model' export { ExtensionMessage } from '../amazonq/webview/ui/commands' export { CodeReference } from '../codewhispererChat/view/connector/connector' export { extractAuthFollowUp } from './util/authUtils' -export { Messenger } from './commons/connector/baseMessenger' export * as secondaryAuth from '../auth/secondaryAuth' export * as authConnection from '../auth/connection' export * as featureConfig from './webview/generators/featureConfig' diff --git a/packages/core/src/amazonq/indexNode.ts b/packages/core/src/amazonq/indexNode.ts index 628b5d626cd..ccc01dc2832 100644 --- a/packages/core/src/amazonq/indexNode.ts +++ b/packages/core/src/amazonq/indexNode.ts @@ -7,6 +7,4 @@ * These agents have underlying requirements on node dependencies (e.g. jsdom, admzip) */ export { init as cwChatAppInit } from '../codewhispererChat/app' -export { init as featureDevChatAppInit } from '../amazonqFeatureDev/app' // TODO: Remove this export { init as gumbyChatAppInit } from '../amazonqGumby/app' -export { init as docChatAppInit } from '../amazonqDoc/app' // TODO: Remove this diff --git a/packages/core/src/amazonq/session/sessionState.ts b/packages/core/src/amazonq/session/sessionState.ts deleted file mode 100644 index 1f206c23159..00000000000 --- a/packages/core/src/amazonq/session/sessionState.ts +++ /dev/null @@ -1,432 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { ToolkitError } from '../../shared/errors' -import globals from '../../shared/extensionGlobals' -import { getLogger } from '../../shared/logger/logger' -import { AmazonqCreateUpload, Span, telemetry } from '../../shared/telemetry/telemetry' -import { VirtualFileSystem } from '../../shared/virtualFilesystem' -import { CodeReference, UploadHistory } from '../webview/ui/connector' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { randomUUID } from '../../shared/crypto' -import { i18n } from '../../shared/i18n-helper' -import { - CodeGenerationStatus, - CurrentWsFolders, - DeletedFileInfo, - DevPhase, - NewFileInfo, - SessionState, - SessionStateAction, - SessionStateConfig, - SessionStateInteraction, - SessionStatePhase, -} from '../commons/types' -import { prepareRepoData, getDeletedFileInfos, registerNewFiles, PrepareRepoDataOptions } from '../util/files' -import { uploadCode } from '../util/upload' -import { truncate } from '../../shared/utilities/textUtilities' - -export const EmptyCodeGenID = 'EMPTY_CURRENT_CODE_GENERATION_ID' -export const RunCommandLogFileName = '.amazonq/dev/run_command.log' - -export interface BaseMessenger { - sendAnswer(params: any): void - sendUpdatePlaceholder?(tabId: string, message: string): void -} - -export abstract class CodeGenBase { - private pollCount = 360 - private requestDelay = 5000 - public tokenSource: vscode.CancellationTokenSource - public phase: SessionStatePhase = DevPhase.CODEGEN - public readonly conversationId: string - public readonly uploadId: string - public currentCodeGenerationId?: string - public isCancellationRequested?: boolean - - constructor( - protected config: SessionStateConfig, - public tabID: string - ) { - this.tokenSource = new vscode.CancellationTokenSource() - this.conversationId = config.conversationId - this.uploadId = config.uploadId - this.currentCodeGenerationId = config.currentCodeGenerationId || EmptyCodeGenID - } - - protected abstract handleProgress(messenger: BaseMessenger, action: SessionStateAction, detail?: string): void - protected abstract getScheme(): string - protected abstract getTimeoutErrorCode(): string - protected abstract handleGenerationComplete( - messenger: BaseMessenger, - newFileInfo: NewFileInfo[], - action: SessionStateAction - ): void - - async generateCode({ - messenger, - fs, - codeGenerationId, - telemetry: telemetry, - workspaceFolders, - action, - }: { - messenger: BaseMessenger - fs: VirtualFileSystem - codeGenerationId: string - telemetry: any - workspaceFolders: CurrentWsFolders - action: SessionStateAction - }): Promise<{ - newFiles: NewFileInfo[] - deletedFiles: DeletedFileInfo[] - references: CodeReference[] - codeGenerationRemainingIterationCount?: number - codeGenerationTotalIterationCount?: number - }> { - let codeGenerationRemainingIterationCount = undefined - let codeGenerationTotalIterationCount = undefined - for ( - let pollingIteration = 0; - pollingIteration < this.pollCount && !this.isCancellationRequested; - ++pollingIteration - ) { - const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId) - codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount - codeGenerationTotalIterationCount = codegenResult.codeGenerationTotalIterationCount - - getLogger().debug(`Codegen response: %O`, codegenResult) - telemetry.setCodeGenerationResult(codegenResult.codeGenerationStatus.status) - - switch (codegenResult.codeGenerationStatus.status as CodeGenerationStatus) { - case CodeGenerationStatus.COMPLETE: { - const { newFileContents, deletedFiles, references } = - await this.config.proxyClient.exportResultArchive(this.conversationId) - - const logFileInfo = newFileContents.find( - (file: { zipFilePath: string; fileContent: string }) => - file.zipFilePath === RunCommandLogFileName - ) - if (logFileInfo) { - logFileInfo.fileContent = truncate(logFileInfo.fileContent, 10000000, '\n... [truncated]') // Limit to max 20MB - getLogger().info(`sessionState: Run Command logs, ${logFileInfo.fileContent}`) - newFileContents.splice(newFileContents.indexOf(logFileInfo), 1) - } - - const newFileInfo = registerNewFiles( - fs, - newFileContents, - this.uploadId, - workspaceFolders, - this.conversationId, - this.getScheme() - ) - telemetry.setNumberOfFilesGenerated(newFileInfo.length) - - this.handleGenerationComplete(messenger, newFileInfo, action) - - return { - newFiles: newFileInfo, - deletedFiles: getDeletedFileInfos(deletedFiles, workspaceFolders), - references, - codeGenerationRemainingIterationCount, - codeGenerationTotalIterationCount, - } - } - case CodeGenerationStatus.PREDICT_READY: - case CodeGenerationStatus.IN_PROGRESS: { - if (codegenResult.codeGenerationStatusDetail) { - this.handleProgress(messenger, action, codegenResult.codeGenerationStatusDetail) - } - await new Promise((f) => globals.clock.setTimeout(f, this.requestDelay)) - break - } - case CodeGenerationStatus.PREDICT_FAILED: - case CodeGenerationStatus.DEBATE_FAILED: - case CodeGenerationStatus.FAILED: { - throw this.handleError(messenger, codegenResult) - } - default: { - const errorMessage = `Unknown status: ${codegenResult.codeGenerationStatus.status}\n` - throw new ToolkitError(errorMessage, { code: 'UnknownCodeGenError' }) - } - } - } - - if (!this.isCancellationRequested) { - const errorMessage = i18n('AWS.amazonq.featureDev.error.codeGen.timeout') - throw new ToolkitError(errorMessage, { code: this.getTimeoutErrorCode() }) - } - - return { - newFiles: [], - deletedFiles: [], - references: [], - codeGenerationRemainingIterationCount: codeGenerationRemainingIterationCount, - codeGenerationTotalIterationCount: codeGenerationTotalIterationCount, - } - } - - protected abstract handleError(messenger: BaseMessenger, codegenResult: any): Error -} - -export abstract class BasePrepareCodeGenState implements SessionState { - public tokenSource: vscode.CancellationTokenSource - public readonly phase = DevPhase.CODEGEN - public uploadId: string - public conversationId: string - - constructor( - protected config: SessionStateConfig, - public filePaths: NewFileInfo[], - public deletedFiles: DeletedFileInfo[], - public references: CodeReference[], - public tabID: string, - public currentIteration: number, - public codeGenerationRemainingIterationCount?: number, - public codeGenerationTotalIterationCount?: number, - public uploadHistory: UploadHistory = {}, - public superTokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(), - public currentCodeGenerationId?: string, - public codeGenerationId?: string - ) { - this.tokenSource = superTokenSource || new vscode.CancellationTokenSource() - this.uploadId = config.uploadId - this.currentCodeGenerationId = currentCodeGenerationId - this.conversationId = config.conversationId - this.uploadHistory = uploadHistory - this.codeGenerationId = codeGenerationId - } - - updateWorkspaceRoot(workspaceRoot: string) { - this.config.workspaceRoots = [workspaceRoot] - } - - protected createNextState( - config: SessionStateConfig, - StateClass?: new ( - config: SessionStateConfig, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - currentIteration: number, - uploadHistory: UploadHistory, - codeGenerationRemainingIterationCount?: number, - codeGenerationTotalIterationCount?: number - ) => SessionState - ): SessionState { - return new StateClass!( - config, - this.filePaths, - this.deletedFiles, - this.references, - this.tabID, - this.currentIteration, - this.uploadHistory - ) - } - - protected abstract preUpload(action: SessionStateAction): void - protected abstract postUpload(action: SessionStateAction): void - - async interact(action: SessionStateAction): Promise { - this.preUpload(action) - const uploadId = await telemetry.amazonq_createUpload.run(async (span) => { - span.record({ - amazonqConversationId: this.config.conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, - }) - const { zipFileBuffer, zipFileChecksum } = await this.prepareProjectZip( - this.config.workspaceRoots, - this.config.workspaceFolders, - span, - { telemetry: action.telemetry } - ) - const uploadId = randomUUID() - const { uploadUrl, kmsKeyArn } = await this.config.proxyClient.createUploadUrl( - this.config.conversationId, - zipFileChecksum, - zipFileBuffer.length, - uploadId - ) - - await uploadCode(uploadUrl, zipFileBuffer, zipFileChecksum, kmsKeyArn) - this.postUpload(action) - - return uploadId - }) - - this.uploadId = uploadId - const nextState = this.createNextState({ ...this.config, uploadId }) - return nextState.interact(action) - } - - protected async prepareProjectZip( - workspaceRoots: string[], - workspaceFolders: CurrentWsFolders, - span: Span, - options: PrepareRepoDataOptions - ) { - return await prepareRepoData(workspaceRoots, workspaceFolders, span, options) - } -} - -export interface CodeGenerationParams { - messenger: BaseMessenger - fs: VirtualFileSystem - codeGenerationId: string - telemetry: any - workspaceFolders: CurrentWsFolders -} - -export interface CreateNextStateParams { - filePaths: NewFileInfo[] - deletedFiles: DeletedFileInfo[] - references: CodeReference[] - currentIteration: number - remainingIterations?: number - totalIterations?: number - uploadHistory: UploadHistory - tokenSource: vscode.CancellationTokenSource - currentCodeGenerationId?: string - codeGenerationId?: string -} - -export abstract class BaseCodeGenState extends CodeGenBase implements SessionState { - constructor( - config: SessionStateConfig, - public filePaths: NewFileInfo[], - public deletedFiles: DeletedFileInfo[], - public references: CodeReference[], - tabID: string, - public currentIteration: number, - public uploadHistory: UploadHistory, - public codeGenerationRemainingIterationCount?: number, - public codeGenerationTotalIterationCount?: number - ) { - super(config, tabID) - } - - protected createNextState( - config: SessionStateConfig, - params: CreateNextStateParams, - StateClass?: new ( - config: SessionStateConfig, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - references: CodeReference[], - tabID: string, - currentIteration: number, - remainingIterations?: number, - totalIterations?: number, - uploadHistory?: UploadHistory, - tokenSource?: vscode.CancellationTokenSource, - currentCodeGenerationId?: string, - codeGenerationId?: string - ) => SessionState - ): SessionState { - return new StateClass!( - config, - params.filePaths, - params.deletedFiles, - params.references, - this.tabID, - params.currentIteration, - params.remainingIterations, - params.totalIterations, - params.uploadHistory, - params.tokenSource, - params.currentCodeGenerationId, - params.codeGenerationId - ) - } - - async interact(action: SessionStateAction): Promise { - return telemetry.amazonq_codeGenerationInvoke.run(async (span) => { - try { - action.tokenSource?.token.onCancellationRequested(() => { - this.isCancellationRequested = true - if (action.tokenSource) { - this.tokenSource = action.tokenSource - } - }) - - span.record({ - amazonqConversationId: this.config.conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, - }) - - action.telemetry.setGenerateCodeIteration(this.currentIteration) - action.telemetry.setGenerateCodeLastInvocationTime() - - const codeGenerationId = randomUUID() - await this.startCodeGeneration(action, codeGenerationId) - - const codeGeneration = await this.generateCode({ - messenger: action.messenger, - fs: action.fs, - codeGenerationId, - telemetry: action.telemetry, - workspaceFolders: this.config.workspaceFolders, - action, - }) - - if (codeGeneration && !action.tokenSource?.token.isCancellationRequested) { - this.config.currentCodeGenerationId = codeGenerationId - this.currentCodeGenerationId = codeGenerationId - } - - this.filePaths = codeGeneration.newFiles - this.deletedFiles = codeGeneration.deletedFiles - this.references = codeGeneration.references - this.codeGenerationRemainingIterationCount = codeGeneration.codeGenerationRemainingIterationCount - this.codeGenerationTotalIterationCount = codeGeneration.codeGenerationTotalIterationCount - this.currentIteration = - this.codeGenerationRemainingIterationCount && this.codeGenerationTotalIterationCount - ? this.codeGenerationTotalIterationCount - this.codeGenerationRemainingIterationCount - : this.currentIteration + 1 - - if (action.uploadHistory && !action.uploadHistory[codeGenerationId] && codeGenerationId) { - action.uploadHistory[codeGenerationId] = { - timestamp: Date.now(), - uploadId: this.config.uploadId, - filePaths: codeGeneration.newFiles, - deletedFiles: codeGeneration.deletedFiles, - tabId: this.tabID, - } - } - - action.telemetry.setAmazonqNumberOfReferences(this.references.length) - action.telemetry.recordUserCodeGenerationTelemetry(span, this.conversationId) - - const nextState = this.createNextState(this.config, { - filePaths: this.filePaths, - deletedFiles: this.deletedFiles, - references: this.references, - currentIteration: this.currentIteration, - remainingIterations: this.codeGenerationRemainingIterationCount, - totalIterations: this.codeGenerationTotalIterationCount, - uploadHistory: action.uploadHistory ? action.uploadHistory : {}, - tokenSource: this.tokenSource, - currentCodeGenerationId: this.currentCodeGenerationId, - codeGenerationId, - }) - - return { - nextState, - interaction: {}, - } - } catch (e) { - throw e instanceof ToolkitError - ? e - : ToolkitError.chain(e, 'Server side error', { code: 'UnhandledCodeGenServerSideError' }) - } - }) - } - - protected abstract startCodeGeneration(action: SessionStateAction, codeGenerationId: string): Promise -} diff --git a/packages/core/src/amazonq/util/files.ts b/packages/core/src/amazonq/util/files.ts deleted file mode 100644 index afa0b674928..00000000000 --- a/packages/core/src/amazonq/util/files.ts +++ /dev/null @@ -1,301 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as path from 'path' -import { - collectFiles, - CollectFilesFilter, - defaultExcludePatterns, - getWorkspaceFoldersByPrefixes, -} from '../../shared/utilities/workspaceUtils' - -import { PrepareRepoFailedError } from '../../amazonqFeatureDev/errors' -import { getLogger } from '../../shared/logger/logger' -import { maxFileSizeBytes } from '../../amazonqFeatureDev/limits' -import { CurrentWsFolders, DeletedFileInfo, NewFileInfo, NewFileZipContents } from '../../amazonqDoc/types' -import { ContentLengthError, hasCode, ToolkitError } from '../../shared/errors' -import { AmazonqCreateUpload, Span, telemetry as amznTelemetry, telemetry } from '../../shared/telemetry/telemetry' -import { maxRepoSizeBytes } from '../../amazonqFeatureDev/constants' -import { isCodeFile } from '../../shared/filetypes' -import { fs } from '../../shared/fs/fs' -import { VirtualFileSystem } from '../../shared/virtualFilesystem' -import { VirtualMemoryFile } from '../../shared/virtualMemoryFile' -import { CodeWhispererSettings } from '../../codewhisperer/util/codewhispererSettings' -import { ZipStream } from '../../shared/utilities/zipStream' -import { isPresent } from '../../shared/utilities/collectionUtils' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { TelemetryHelper } from '../util/telemetryHelper' - -export const SvgFileExtension = '.svg' - -export async function checkForDevFile(root: string) { - const devFilePath = root + '/devfile.yaml' - const hasDevFile = await fs.existsFile(devFilePath) - return hasDevFile -} - -function isInfraDiagramFile(relativePath: string) { - return ( - relativePath.toLowerCase().endsWith(path.join('docs', 'infra.dot')) || - relativePath.toLowerCase().endsWith(path.join('docs', 'infra.svg')) - ) -} - -export type PrepareRepoDataOptions = { - telemetry?: TelemetryHelper - zip?: ZipStream - isIncludeInfraDiagram?: boolean -} - -/** - * given the root path of the repo it zips its files in memory and generates a checksum for it. - */ -export async function prepareRepoData( - repoRootPaths: string[], - workspaceFolders: CurrentWsFolders, - span: Span, - options?: PrepareRepoDataOptions -) { - try { - const telemetry = options?.telemetry - const isIncludeInfraDiagram = options?.isIncludeInfraDiagram ?? false - const zip = options?.zip ?? new ZipStream() - - const autoBuildSetting = CodeWhispererSettings.instance.getAutoBuildSetting() - const useAutoBuildFeature = autoBuildSetting[repoRootPaths[0]] ?? false - const excludePatterns: string[] = [] - let filterFn: CollectFilesFilter | undefined = undefined - - // We only respect gitignore file rules if useAutoBuildFeature is on, this is to avoid dropping necessary files for building the code (e.g. png files imported in js code) - if (!useAutoBuildFeature) { - if (isIncludeInfraDiagram) { - // ensure svg is not filtered out by files search - excludePatterns.push(...defaultExcludePatterns.filter((p) => !p.endsWith(SvgFileExtension))) - // ensure only infra diagram is included from all svg files - filterFn = (relativePath: string) => { - if (!relativePath.toLowerCase().endsWith(SvgFileExtension)) { - return false - } - return !isInfraDiagramFile(relativePath) - } - } else { - excludePatterns.push(...defaultExcludePatterns) - } - } - - const files = await collectFiles(repoRootPaths, workspaceFolders, { - maxTotalSizeBytes: maxRepoSizeBytes, - excludeByGitIgnore: true, - excludePatterns: excludePatterns, - filterFn: filterFn, - }) - - let totalBytes = 0 - const ignoredExtensionMap = new Map() - for (const file of files) { - let fileSize - try { - fileSize = (await fs.stat(file.fileUri)).size - } catch (error) { - if (hasCode(error) && error.code === 'ENOENT') { - // No-op: Skip if file does not exist - continue - } - throw error - } - const isCodeFile_ = isCodeFile(file.relativeFilePath) - const isDevFile = file.relativeFilePath === 'devfile.yaml' - const isInfraDiagramFileExt = isInfraDiagramFile(file.relativeFilePath) - - let isExcludeFile = fileSize >= maxFileSizeBytes - // When useAutoBuildFeature is on, only respect the gitignore rules filtered earlier and apply the size limit - if (!isExcludeFile && !useAutoBuildFeature) { - isExcludeFile = isDevFile || (!isCodeFile_ && (!isIncludeInfraDiagram || !isInfraDiagramFileExt)) - } - - if (isExcludeFile) { - if (!isCodeFile_) { - const re = /(?:\.([^.]+))?$/ - const extensionArray = re.exec(file.relativeFilePath) - const extension = extensionArray?.length ? extensionArray[1] : undefined - if (extension) { - const currentCount = ignoredExtensionMap.get(extension) - - ignoredExtensionMap.set(extension, (currentCount ?? 0) + 1) - } - } - continue - } - - totalBytes += fileSize - // Paths in zip should be POSIX compliant regardless of OS - // Reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT - const posixPath = file.zipFilePath.split(path.sep).join(path.posix.sep) - - try { - zip.writeFile(file.fileUri.fsPath, posixPath) - } catch (error) { - if (error instanceof Error && error.message.includes('File not found')) { - // No-op: Skip if file was deleted or does not exist - // Reference: https://github.com/cthackers/adm-zip/blob/1cd32f7e0ad3c540142a76609bb538a5cda2292f/adm-zip.js#L296-L321 - continue - } - throw error - } - } - - const iterator = ignoredExtensionMap.entries() - - for (let i = 0; i < ignoredExtensionMap.size; i++) { - const iteratorValue = iterator.next().value - if (iteratorValue) { - const [key, value] = iteratorValue - await amznTelemetry.amazonq_bundleExtensionIgnored.run(async (bundleSpan) => { - const event = { - filenameExt: key, - count: value, - } - - bundleSpan.record(event) - }) - } - } - - if (telemetry) { - telemetry.setRepositorySize(totalBytes) - } - - span.record({ amazonqRepositorySize: totalBytes }) - const zipResult = await zip.finalize() - - const zipFileBuffer = zipResult.streamBuffer.getContents() || Buffer.from('') - return { - zipFileBuffer, - zipFileChecksum: zipResult.hash, - } - } catch (error) { - getLogger().debug(`Failed to prepare repo: ${error}`) - if (error instanceof ToolkitError && error.code === 'ContentLengthError') { - throw new ContentLengthError(error.message) - } - throw new PrepareRepoFailedError() - } -} - -/** - * gets the absolute path from a zip path - * @param zipFilePath the path in the zip file - * @param workspacesByPrefix the workspaces with generated prefixes - * @param workspaceFolders all workspace folders - * @returns all possible path info - */ -export function getPathsFromZipFilePath( - zipFilePath: string, - workspacesByPrefix: { [prefix: string]: vscode.WorkspaceFolder } | undefined, - workspaceFolders: CurrentWsFolders -): { - absolutePath: string - relativePath: string - workspaceFolder: vscode.WorkspaceFolder -} { - // when there is just a single workspace folder, there is no prefixing - if (workspacesByPrefix === undefined) { - return { - absolutePath: path.join(workspaceFolders[0].uri.fsPath, zipFilePath), - relativePath: zipFilePath, - workspaceFolder: workspaceFolders[0], - } - } - // otherwise the first part of the zipPath is the prefix - const prefix = zipFilePath.substring(0, zipFilePath.indexOf(path.sep)) - const workspaceFolder = - workspacesByPrefix[prefix] ?? - (workspacesByPrefix[Object.values(workspacesByPrefix).find((val) => val.index === 0)?.name ?? ''] || undefined) - if (workspaceFolder === undefined) { - throw new ToolkitError(`Could not find workspace folder for prefix ${prefix}`) - } - return { - absolutePath: path.join(workspaceFolder.uri.fsPath, zipFilePath.substring(prefix.length + 1)), - relativePath: zipFilePath.substring(prefix.length + 1), - workspaceFolder, - } -} - -export function getDeletedFileInfos(deletedFiles: string[], workspaceFolders: CurrentWsFolders): DeletedFileInfo[] { - const workspaceFolderPrefixes = getWorkspaceFoldersByPrefixes(workspaceFolders) - return deletedFiles - .map((deletedFilePath) => { - const prefix = - workspaceFolderPrefixes === undefined - ? '' - : deletedFilePath.substring(0, deletedFilePath.indexOf(path.sep)) - const folder = workspaceFolderPrefixes === undefined ? workspaceFolders[0] : workspaceFolderPrefixes[prefix] - if (folder === undefined) { - getLogger().error(`No workspace folder found for file: ${deletedFilePath} and prefix: ${prefix}`) - return undefined - } - const prefixLength = workspaceFolderPrefixes === undefined ? 0 : prefix.length + 1 - return { - zipFilePath: deletedFilePath, - workspaceFolder: folder, - relativePath: deletedFilePath.substring(prefixLength), - rejected: false, - changeApplied: false, - } - }) - .filter(isPresent) -} - -export function registerNewFiles( - fs: VirtualFileSystem, - newFileContents: NewFileZipContents[], - uploadId: string, - workspaceFolders: CurrentWsFolders, - conversationId: string, - scheme: string -): NewFileInfo[] { - const result: NewFileInfo[] = [] - const workspaceFolderPrefixes = getWorkspaceFoldersByPrefixes(workspaceFolders) - for (const { zipFilePath, fileContent } of newFileContents) { - const encoder = new TextEncoder() - const contents = encoder.encode(fileContent) - const generationFilePath = path.join(uploadId, zipFilePath) - const uri = vscode.Uri.from({ scheme, path: generationFilePath }) - fs.registerProvider(uri, new VirtualMemoryFile(contents)) - const prefix = - workspaceFolderPrefixes === undefined ? '' : zipFilePath.substring(0, zipFilePath.indexOf(path.sep)) - const folder = - workspaceFolderPrefixes === undefined - ? workspaceFolders[0] - : (workspaceFolderPrefixes[prefix] ?? - workspaceFolderPrefixes[ - Object.values(workspaceFolderPrefixes).find((val) => val.index === 0)?.name ?? '' - ]) - if (folder === undefined) { - telemetry.toolkit_trackScenario.emit({ - count: 1, - amazonqConversationId: conversationId, - credentialStartUrl: AuthUtil.instance.startUrl, - scenario: 'wsOrphanedDocuments', - }) - getLogger().error(`No workspace folder found for file: ${zipFilePath} and prefix: ${prefix}`) - continue - } - result.push({ - zipFilePath, - fileContent, - virtualMemoryUri: uri, - workspaceFolder: folder, - relativePath: zipFilePath.substring( - workspaceFolderPrefixes === undefined ? 0 : prefix.length > 0 ? prefix.length + 1 : 0 - ), - rejected: false, - changeApplied: false, - }) - } - - return result -} diff --git a/packages/core/src/amazonq/util/upload.ts b/packages/core/src/amazonq/util/upload.ts index bd4ff26cc45..92e88fbea0e 100644 --- a/packages/core/src/amazonq/util/upload.ts +++ b/packages/core/src/amazonq/util/upload.ts @@ -5,11 +5,10 @@ import request, { RequestError } from '../../shared/request' import { getLogger } from '../../shared/logger/logger' -import { featureName } from '../../amazonqFeatureDev/constants' -import { UploadCodeError, UploadURLExpired } from '../../amazonqFeatureDev/errors' -import { ToolkitError } from '../../shared/errors' +import { ToolkitError, UploadCodeError, UploadURLExpired } from '../../shared/errors' import { i18n } from '../../shared/i18n-helper' +import { featureName } from '../../shared/constants' /** * uploadCode diff --git a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts index 68983b6c188..ee20b9b0726 100644 --- a/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/amazonqCommonsConnector.ts @@ -33,14 +33,12 @@ export interface CodeReference { export class Connector { private readonly sendMessageToExtension private readonly onWelcomeFollowUpClicked - private readonly onNewTab private readonly handleCommand private readonly sendStaticMessage constructor(props: ConnectorProps) { this.sendMessageToExtension = props.sendMessageToExtension this.onWelcomeFollowUpClicked = props.onWelcomeFollowUpClicked - this.onNewTab = props.onNewTab this.handleCommand = props.handleCommand this.sendStaticMessage = props.sendStaticMessages } @@ -61,10 +59,7 @@ export class Connector { } handleMessageReceive = async (messageData: any): Promise => { - if (messageData.command === 'showExploreAgentsView') { - this.onNewTab('agentWalkthrough') - return - } else if (messageData.command === 'review') { + if (messageData.command === 'review') { // tabID does not exist when calling from QuickAction Menu bar this.handleCommand({ command: '/review' }, '') return diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index cc1b010375a..97821fc842f 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -596,10 +596,6 @@ export class Connector { this.cwChatConnector.onCustomFormAction(tabId, action) } break - case 'agentWalkthrough': { - this.amazonqCommonsConnector.onCustomFormAction(tabId, action) - break - } } } } diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 54696982ae0..c6df42f0566 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -32,7 +32,6 @@ import { DiffTreeFileInfo } from './diffTree/types' import { FeatureContext } from '../../../shared/featureConfig' import { tryNewMap } from '../../util/functionUtils' import { welcomeScreenTabData } from './walkthrough/welcome' -import { agentWalkthroughDataModel } from './walkthrough/agent' import { createClickTelemetry, createOpenAgentTelemetry } from './telemetry/actions' import { disclaimerAcknowledgeButtonId, disclaimerCard } from './texts/disclaimer' import { DetailedListSheetProps } from '@aws/mynah-ui/dist/components/detailed-list/detailed-list-sheet' @@ -783,19 +782,6 @@ export class WebviewUIHandler { this.postMessage(createClickTelemetry('amazonq-welcome-quick-start-button')) return } - case 'explore': { - const newTabId = this.mynahUI?.updateStore('', agentWalkthroughDataModel) - if (newTabId === undefined) { - this.mynahUI?.notify({ - content: uiComponentsTexts.noMoreTabsTooltip, - type: NotificationType.WARNING, - }) - return - } - this.tabsStorage.updateTabTypeFromUnknown(newTabId, 'agentWalkthrough') - this.postMessage(createClickTelemetry('amazonq-welcome-explore-button')) - return - } default: { this.connector?.onCustomFormAction(tabId, messageId, action, eventId) return diff --git a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts index 0cc7740f2ec..4ca8b4cc10e 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts @@ -25,11 +25,6 @@ export class QuickActionGenerator { } public generateForTab(tabType: TabType): QuickActionCommandGroup[] { - // agentWalkthrough is static and doesn't have any quick actions - if (tabType === 'agentWalkthrough') { - return [] - } - // TODO: Update acc to UX const quickActionCommands = [ { @@ -101,7 +96,7 @@ export class QuickActionGenerator { ].filter((section) => section.commands.length > 0) const commandUnavailability: Record< - Exclude, + Exclude, { description: string unavailableItems: string[] diff --git a/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts b/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts index 92fa7c5a07e..2a803759fd0 100644 --- a/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts +++ b/packages/core/src/amazonq/webview/ui/storages/tabsStorage.ts @@ -4,7 +4,7 @@ */ export type TabStatus = 'free' | 'busy' | 'dead' -const TabTypes = ['cwc', 'gumby', 'review', 'agentWalkthrough', 'welcome', 'unknown'] as const +const TabTypes = ['cwc', 'gumby', 'review', 'welcome', 'unknown'] as const export type TabType = (typeof TabTypes)[number] export function isTabType(value: string): value is TabType { return (TabTypes as readonly string[]).includes(value) diff --git a/packages/core/src/amazonq/webview/ui/tabs/constants.ts b/packages/core/src/amazonq/webview/ui/tabs/constants.ts index ead70679b7f..0872b829a6a 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/constants.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/constants.ts @@ -44,7 +44,7 @@ export const commonTabData: TabTypeData = { contextCommands: [workspaceCommand], } -export const TabTypeDataMap: Record, TabTypeData> = { +export const TabTypeDataMap: Record, TabTypeData> = { unknown: commonTabData, cwc: commonTabData, gumby: { diff --git a/packages/core/src/amazonq/webview/ui/tabs/generator.ts b/packages/core/src/amazonq/webview/ui/tabs/generator.ts index 2331a0721c7..68b758d51cb 100644 --- a/packages/core/src/amazonq/webview/ui/tabs/generator.ts +++ b/packages/core/src/amazonq/webview/ui/tabs/generator.ts @@ -8,7 +8,6 @@ import { TabType } from '../storages/tabsStorage' import { FollowUpGenerator } from '../followUps/generator' import { QuickActionGenerator } from '../quickActions/generator' import { qChatIntroMessageForSMUS, TabTypeDataMap } from './constants' -import { agentWalkthroughDataModel } from '../walkthrough/agent' import { FeatureContext } from '../../../../shared/featureConfig' import { RegionProfile } from '../../../../codewhisperer/models/model' @@ -43,10 +42,6 @@ export class TabDataGenerator { taskName?: string, isSMUS?: boolean ): MynahUIDataModel { - if (tabType === 'agentWalkthrough') { - return agentWalkthroughDataModel - } - if (tabType === 'welcome') { return {} } @@ -86,7 +81,7 @@ export class TabDataGenerator { } private getContextCommands(tabType: TabType): QuickActionCommandGroup[] | undefined { - if (tabType === 'agentWalkthrough' || tabType === 'welcome') { + if (tabType === 'welcome') { return } diff --git a/packages/core/src/amazonq/webview/ui/walkthrough/agent.ts b/packages/core/src/amazonq/webview/ui/walkthrough/agent.ts deleted file mode 100644 index f4a5add7aa1..00000000000 --- a/packages/core/src/amazonq/webview/ui/walkthrough/agent.ts +++ /dev/null @@ -1,201 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemContent, ChatItemType, MynahIcons, MynahUIDataModel } from '@aws/mynah-ui' - -function createdTabbedData(examples: string[], agent: string): ChatItemContent['tabbedContent'] { - const exampleText = examples.map((example) => `- ${example}`).join('\n') - return [ - { - label: 'Examples', - value: 'examples', - content: { - body: `**Example use cases:**\n${exampleText}\n\nEnter ${agent} in Q Chat to get started`, - }, - }, - ] -} - -export const agentWalkthroughDataModel: MynahUIDataModel = { - tabBackground: false, - compactMode: false, - tabTitle: 'Explore', - promptInputVisible: false, - tabHeaderDetails: { - icon: MynahIcons.ASTERISK, - title: 'Amazon Q Developer agents capabilities', - description: '', - }, - chatItems: [ - { - type: ChatItemType.ANSWER, - snapToTop: true, - hoverEffect: true, - body: `### Feature development -Implement features or make changes across your workspace, all from a single prompt. -`, - icon: MynahIcons.CODE_BLOCK, - footer: { - tabbedContent: createdTabbedData( - [ - '/dev update app.py to add a new api', - '/dev fix the error', - '/dev add a new button to sort by ', - ], - '/dev' - ), - }, - buttons: [ - { - status: 'clear', - id: `user-guide-featuredev`, - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-featuredev', - text: `Quick start with **/dev**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Unit test generation -Automatically generate unit tests for your active file. -`, - icon: MynahIcons.BUG, - footer: { - tabbedContent: createdTabbedData( - ['Generate tests for specific functions', 'Generate tests for null and empty inputs'], - '/test' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-testgen', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-testgen', - text: `Quick start with **/test**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Documentation generation -Create and update READMEs for better documented code. -`, - icon: MynahIcons.CHECK_LIST, - footer: { - tabbedContent: createdTabbedData( - [ - 'Generate new READMEs for your project', - 'Update existing READMEs with recent code changes', - 'Request specific changes to a README', - ], - '/doc' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-doc', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-doc', - text: `Quick start with **/doc**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Code reviews -Review code for issues, then get suggestions to fix your code instantaneously. -`, - icon: MynahIcons.TRANSFORM, - footer: { - tabbedContent: createdTabbedData( - [ - 'Review code for security vulnerabilities and code quality issues', - 'Get detailed explanations about code issues', - 'Apply automatic code fixes to your files', - ], - '/review' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-review', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-review', - text: `Quick start with **/review**`, - }, - ], - }, - { - type: ChatItemType.ANSWER, - hoverEffect: true, - body: `### Transformation -Upgrade library and language versions in your codebase. -`, - icon: MynahIcons.TRANSFORM, - footer: { - tabbedContent: createdTabbedData( - ['Upgrade Java language and dependency versions', 'Convert embedded SQL code in Java apps'], - '/transform' - ), - }, - buttons: [ - { - status: 'clear', - id: 'user-guide-gumby', - disabled: false, - text: 'Read user guide', - }, - { - status: 'main', - disabled: false, - flash: 'once', - fillState: 'hover', - icon: MynahIcons.RIGHT_OPEN, - id: 'quick-start-gumby', - text: `Quick start with **/transform**`, - }, - ], - }, - ], -} diff --git a/packages/core/src/amazonqDoc/app.ts b/packages/core/src/amazonqDoc/app.ts deleted file mode 100644 index 52985b82a00..00000000000 --- a/packages/core/src/amazonqDoc/app.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { ChatControllerEventEmitters, DocController } from './controllers/chat/controller' -import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessageListener } from '../amazonq/messages/messageListener' -import { fromQueryToParameters } from '../shared/utilities/uriUtils' -import { getLogger } from '../shared/logger/logger' -import { AuthUtil } from '../codewhisperer/util/authUtil' -import { debounce } from 'lodash' -import { DocChatSessionStorage } from './storages/chatSession' -import { UIMessageListener } from './views/actions/uiMessageListener' -import globals from '../shared/extensionGlobals' -import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' -import { docChat, docScheme } from './constants' -import { TabIdNotFoundError } from '../amazonqFeatureDev/errors' -import { DocMessenger } from './messenger' - -export function init(appContext: AmazonQAppInitContext) { - const docChatControllerEventEmitters: ChatControllerEventEmitters = { - processHumanChatMessage: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - stopResponse: new vscode.EventEmitter(), - tabOpened: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - formActionClicked: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - insertCodeAtPositionClicked: new vscode.EventEmitter(), - fileClicked: new vscode.EventEmitter(), - } - - const messenger = new DocMessenger( - new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher()), - docChat - ) - const sessionStorage = new DocChatSessionStorage(messenger) - - new DocController( - docChatControllerEventEmitters, - messenger, - sessionStorage, - appContext.onDidChangeAmazonQVisibility.event - ) - - const docProvider = new (class implements vscode.TextDocumentContentProvider { - async provideTextDocumentContent(uri: vscode.Uri): Promise { - const params = fromQueryToParameters(uri.query) - - const tabID = params.get('tabID') - if (!tabID) { - getLogger().error(`Unable to find tabID from ${uri.toString()}`) - throw new TabIdNotFoundError() - } - - const session = await sessionStorage.getSession(tabID) - const content = await session.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - return decodedContent - } - })() - - const textDocumentProvider = vscode.workspace.registerTextDocumentContentProvider(docScheme, docProvider) - - globals.context.subscriptions.push(textDocumentProvider) - - const docChatUIInputEventEmitter = new vscode.EventEmitter() - - new UIMessageListener({ - chatControllerEventEmitters: docChatControllerEventEmitters, - webViewMessageListener: new MessageListener(docChatUIInputEventEmitter), - }) - - const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' - let authenticatingSessionIDs: string[] = [] - if (authenticated) { - const authenticatingSessions = sessionStorage.getAuthenticatingSessions() - - authenticatingSessionIDs = authenticatingSessions.map((session: any) => session.tabID) - - // We've already authenticated these sessions - for (const session of authenticatingSessions) { - session.isAuthenticating = false - } - } - - messenger.sendAuthenticationUpdate(authenticated, authenticatingSessionIDs) - }, 500) - - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { - return debouncedEvent() - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - return debouncedEvent() - }) -} diff --git a/packages/core/src/amazonqDoc/constants.ts b/packages/core/src/amazonqDoc/constants.ts deleted file mode 100644 index 7b57e7c2ce9..00000000000 --- a/packages/core/src/amazonqDoc/constants.ts +++ /dev/null @@ -1,163 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { MynahIcons, Status } from '@aws/mynah-ui' -import { FollowUpTypes } from '../amazonq/commons/types' -import { NewFileInfo } from './types' -import { i18n } from '../shared/i18n-helper' - -// For uniquely identifiying which chat messages should be routed to Doc -export const docChat = 'docChat' - -export const docScheme = 'aws-doc' - -export const featureName = 'Amazon Q Doc Generation' - -export function getFileSummaryPercentage(input: string): number { - // Split the input string by newline characters - const lines = input.split('\n') - - // Find the line containing "summarized:" - const summaryLine = lines.find((line) => line.includes('summarized:')) - - // If the line is not found, return null - if (!summaryLine) { - return -1 - } - - // Extract the numbers from the summary line - const [summarized, total] = summaryLine.split(':')[1].trim().split(' of ').map(Number) - - // Calculate the percentage - const percentage = (summarized / total) * 100 - - return percentage -} - -const checkIcons = { - wait: '☐', - current: '☐', - done: '☑', -} - -const getIconForStep = (targetStep: number, currentStep: number) => { - return currentStep === targetStep - ? checkIcons.current - : currentStep > targetStep - ? checkIcons.done - : checkIcons.wait -} - -export enum DocGenerationStep { - UPLOAD_TO_S3, - SUMMARIZING_FILES, - GENERATING_ARTIFACTS, -} - -export const docGenerationProgressMessage = (currentStep: DocGenerationStep, mode: Mode) => ` -${mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.creating') : i18n('AWS.amazonq.doc.answer.updating')} - -${getIconForStep(DocGenerationStep.UPLOAD_TO_S3, currentStep)} ${i18n('AWS.amazonq.doc.answer.scanning')} - -${getIconForStep(DocGenerationStep.SUMMARIZING_FILES, currentStep)} ${i18n('AWS.amazonq.doc.answer.summarizing')} - -${getIconForStep(DocGenerationStep.GENERATING_ARTIFACTS, currentStep)} ${i18n('AWS.amazonq.doc.answer.generating')} - - -` - -export const docGenerationSuccessMessage = (mode: Mode) => - mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated') - -export const docRejectConfirmation = 'Your changes have been discarded.' - -export const FolderSelectorFollowUps = [ - { - icon: 'ok' as MynahIcons, - pillText: 'Yes', - prompt: 'Yes', - status: 'success' as Status, - type: FollowUpTypes.ProceedFolderSelection, - }, - { - icon: 'refresh' as MynahIcons, - pillText: 'Change folder', - prompt: 'Change folder', - status: 'info' as Status, - type: FollowUpTypes.ChooseFolder, - }, - { - icon: 'cancel' as MynahIcons, - pillText: 'Cancel', - prompt: 'Cancel', - status: 'error' as Status, - type: FollowUpTypes.CancelFolderSelection, - }, -] - -export const CodeChangeFollowUps = [ - { - pillText: i18n('AWS.amazonq.doc.pillText.accept'), - prompt: i18n('AWS.amazonq.doc.pillText.accept'), - type: FollowUpTypes.AcceptChanges, - icon: 'ok' as MynahIcons, - status: 'success' as Status, - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.makeChanges'), - prompt: i18n('AWS.amazonq.doc.pillText.makeChanges'), - type: FollowUpTypes.MakeChanges, - icon: 'refresh' as MynahIcons, - status: 'info' as Status, - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.reject'), - prompt: i18n('AWS.amazonq.doc.pillText.reject'), - type: FollowUpTypes.RejectChanges, - icon: 'cancel' as MynahIcons, - status: 'error' as Status, - }, -] - -export const NewSessionFollowUps = [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info' as Status, - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info' as Status, - }, -] - -export const SynchronizeDocumentation = { - pillText: i18n('AWS.amazonq.doc.pillText.update'), - prompt: i18n('AWS.amazonq.doc.pillText.update'), - type: FollowUpTypes.SynchronizeDocumentation, -} - -export const EditDocumentation = { - pillText: i18n('AWS.amazonq.doc.pillText.makeChange'), - prompt: i18n('AWS.amazonq.doc.pillText.makeChange'), - type: FollowUpTypes.EditDocumentation, -} - -export enum Mode { - NONE = 'None', - CREATE = 'Create', - SYNC = 'Sync', - EDIT = 'Edit', -} - -/** - * - * @param paths file paths - * @returns the path to a README.md, or undefined if none exist - */ -export const findReadmePath = (paths?: NewFileInfo[]) => { - return paths?.find((path) => /readme\.md$/i.test(path.relativePath)) -} diff --git a/packages/core/src/amazonqDoc/controllers/chat/controller.ts b/packages/core/src/amazonqDoc/controllers/chat/controller.ts deleted file mode 100644 index ab6045e75ce..00000000000 --- a/packages/core/src/amazonqDoc/controllers/chat/controller.ts +++ /dev/null @@ -1,715 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import { EventEmitter } from 'vscode' - -import { - DocGenerationStep, - EditDocumentation, - FolderSelectorFollowUps, - Mode, - NewSessionFollowUps, - SynchronizeDocumentation, - CodeChangeFollowUps, - docScheme, - featureName, - findReadmePath, -} from '../../constants' -import { AuthUtil } from '../../../codewhisperer/util/authUtil' -import { getLogger } from '../../../shared/logger/logger' - -import { Session } from '../../session/session' -import { i18n } from '../../../shared/i18n-helper' -import path from 'path' -import { createSingleFileDialog } from '../../../shared/ui/common/openDialog' - -import { - MonthlyConversationLimitError, - SelectedFolderNotInWorkspaceFolderError, - WorkspaceFolderNotFoundError, - createUserFacingErrorMessage, - getMetricResult, -} from '../../../amazonqFeatureDev/errors' -import { BaseChatSessionStorage } from '../../../amazonq/commons/baseChatStorage' -import { DocMessenger } from '../../messenger' -import { AuthController } from '../../../amazonq/auth/controller' -import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { createAmazonQUri, openDeletedDiff, openDiff } from '../../../amazonq/commons/diff' -import { - getWorkspaceFoldersByPrefixes, - getWorkspaceRelativePath, - isMultiRootWorkspace, -} from '../../../shared/utilities/workspaceUtils' -import { getPathsFromZipFilePath, SvgFileExtension } from '../../../amazonq/util/files' -import { FollowUpTypes } from '../../../amazonq/commons/types' -import { DocGenerationTask, DocGenerationTasks } from '../docGenerationTask' -import { normalize } from '../../../shared/utilities/pathUtils' -import { DevPhase, MetricDataOperationName, MetricDataResult } from '../../types' - -export interface ChatControllerEventEmitters { - readonly processHumanChatMessage: EventEmitter - readonly followUpClicked: EventEmitter - readonly openDiff: EventEmitter - readonly stopResponse: EventEmitter - readonly tabOpened: EventEmitter - readonly tabClosed: EventEmitter - readonly processChatItemVotedMessage: EventEmitter - readonly processChatItemFeedbackMessage: EventEmitter - readonly authClicked: EventEmitter - readonly processResponseBodyLinkClick: EventEmitter - readonly insertCodeAtPositionClicked: EventEmitter - readonly fileClicked: EventEmitter - readonly formActionClicked: EventEmitter -} - -export class DocController { - private readonly scheme = docScheme - private readonly messenger: DocMessenger - private readonly sessionStorage: BaseChatSessionStorage - private authController: AuthController - private docGenerationTasks: DocGenerationTasks - - public constructor( - private readonly chatControllerMessageListeners: ChatControllerEventEmitters, - messenger: DocMessenger, - sessionStorage: BaseChatSessionStorage, - _onDidChangeAmazonQVisibility: vscode.Event - ) { - this.messenger = messenger - this.sessionStorage = sessionStorage - this.authController = new AuthController() - this.docGenerationTasks = new DocGenerationTasks() - - this.chatControllerMessageListeners.processHumanChatMessage.event((data) => { - this.processUserChatMessage(data).catch((e) => { - getLogger().error('processUserChatMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.formActionClicked.event((data) => { - return this.formActionClicked(data) - }) - - this.initializeFollowUps() - - this.chatControllerMessageListeners.stopResponse.event((data) => { - return this.stopResponse(data) - }) - this.chatControllerMessageListeners.tabOpened.event((data) => { - return this.tabOpened(data) - }) - this.chatControllerMessageListeners.tabClosed.event((data) => { - this.tabClosed(data) - }) - this.chatControllerMessageListeners.authClicked.event((data) => { - this.authClicked(data) - }) - this.chatControllerMessageListeners.processResponseBodyLinkClick.event((data) => { - this.processLink(data) - }) - this.chatControllerMessageListeners.fileClicked.event(async (data) => { - return await this.fileClicked(data) - }) - this.chatControllerMessageListeners.openDiff.event(async (data) => { - return await this.openDiff(data) - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - this.sessionStorage.deleteAllSessions() - }) - } - - /** Prompts user to choose a folder in current workspace for README creation/update. - * After user chooses a folder, displays confirmation message to user with selected path. - * - */ - private async folderSelector(data: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: data.tabID, - message: i18n('AWS.amazonq.doc.answer.chooseFolder'), - disableChatInput: true, - }) - - const uri = await createSingleFileDialog({ - canSelectFolders: true, - canSelectFiles: false, - }).prompt() - - const retryFollowUps = FolderSelectorFollowUps.filter( - (followUp) => followUp.type !== FollowUpTypes.ProceedFolderSelection - ) - - if (!(uri instanceof vscode.Uri)) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: data.tabID, - message: i18n('AWS.amazonq.doc.error.noFolderSelected'), - followUps: retryFollowUps, - disableChatInput: true, - }) - // Check that selected folder is a subfolder of the current workspace - } else if (!vscode.workspace.getWorkspaceFolder(uri)) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: data.tabID, - message: new SelectedFolderNotInWorkspaceFolderError().message, - followUps: retryFollowUps, - disableChatInput: true, - }) - } else { - let displayPath = '' - const relativePath = getWorkspaceRelativePath(uri.fsPath) - const docGenerationTask = this.docGenerationTasks.getTask(data.tabID) - if (relativePath) { - // Display path should always include workspace folder name - displayPath = path.join(relativePath.workspaceFolder.name, relativePath.relativePath) - // Only include workspace folder name in API call if multi-root workspace - docGenerationTask.folderPath = normalize( - isMultiRootWorkspace() ? displayPath : relativePath.relativePath - ) - - if (!relativePath.relativePath) { - docGenerationTask.folderLevel = 'ENTIRE_WORKSPACE' - } else { - docGenerationTask.folderLevel = 'SUB_FOLDER' - } - } - - this.messenger.sendFolderConfirmationMessage( - data.tabID, - docGenerationTask.mode === Mode.CREATE - ? i18n('AWS.amazonq.doc.answer.createReadme') - : i18n('AWS.amazonq.doc.answer.updateReadme'), - displayPath, - FolderSelectorFollowUps - ) - this.messenger.sendChatInputEnabled(data.tabID, false) - } - } - - private async openDiff(message: any) { - const tabId: string = message.tabID - const codeGenerationId: string = message.messageId - const zipFilePath: string = message.filePath - const session = await this.sessionStorage.getSession(tabId) - - const workspacePrefixMapping = getWorkspaceFoldersByPrefixes(session.config.workspaceFolders) - const pathInfos = getPathsFromZipFilePath(zipFilePath, workspacePrefixMapping, session.config.workspaceFolders) - - const extension = path.parse(message.filePath).ext - // Only open diffs on files, not directories - if (extension) { - if (message.deleted) { - const name = path.basename(pathInfos.relativePath) - await openDeletedDiff(pathInfos.absolutePath, name, tabId, this.scheme) - } else { - let uploadId = session.uploadId - if (session?.state?.uploadHistory && session.state.uploadHistory[codeGenerationId]) { - uploadId = session?.state?.uploadHistory[codeGenerationId].uploadId - } - const rightPath = path.join(uploadId, zipFilePath) - if (rightPath.toLowerCase().endsWith(SvgFileExtension)) { - const rightPathUri = createAmazonQUri(rightPath, tabId, this.scheme) - const infraDiagramContent = await vscode.workspace.openTextDocument(rightPathUri) - await vscode.window.showTextDocument(infraDiagramContent) - } else { - await openDiff(pathInfos.absolutePath, rightPath, tabId, this.scheme) - } - } - } - } - - private initializeFollowUps(): void { - this.chatControllerMessageListeners.followUpClicked.event(async (data) => { - const session: Session = await this.sessionStorage.getSession(data.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(data.tabID) - - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - return - } - - const workspaceFolderName = vscode.workspace.workspaceFolders?.[0].name || '' - - const authState = await AuthUtil.instance.getChatAuthState() - - if (authState.amazonQ !== 'connected') { - await this.messenger.sendAuthNeededExceptionMessage(authState, data.tabID) - session.isAuthenticating = true - return - } - - const sendFolderConfirmationMessage = (message: string) => { - this.messenger.sendFolderConfirmationMessage( - data.tabID, - message, - workspaceFolderName, - FolderSelectorFollowUps - ) - } - - switch (data.followUp.type) { - case FollowUpTypes.Retry: - if (docGenerationTask.mode === Mode.EDIT) { - this.enableUserInput(data?.tabID) - } else { - await this.tabOpened(data) - } - break - case FollowUpTypes.NewTask: - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - message: i18n('AWS.amazonq.featureDev.answer.newTaskChanges'), - disableChatInput: true, - }) - return this.newTask(data) - case FollowUpTypes.CloseSession: - return this.closeSession(data) - case FollowUpTypes.CreateDocumentation: - docGenerationTask.interactionType = 'GENERATE_README' - docGenerationTask.mode = Mode.CREATE - sendFolderConfirmationMessage(i18n('AWS.amazonq.doc.answer.createReadme')) - break - case FollowUpTypes.ChooseFolder: - await this.folderSelector(data) - break - case FollowUpTypes.SynchronizeDocumentation: - docGenerationTask.mode = Mode.SYNC - sendFolderConfirmationMessage(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - case FollowUpTypes.UpdateDocumentation: - docGenerationTask.interactionType = 'UPDATE_README' - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - followUps: [SynchronizeDocumentation, EditDocumentation], - disableChatInput: true, - }) - break - case FollowUpTypes.EditDocumentation: - docGenerationTask.interactionType = 'EDIT_README' - docGenerationTask.mode = Mode.EDIT - sendFolderConfirmationMessage(i18n('AWS.amazonq.doc.answer.updateReadme')) - break - case FollowUpTypes.MakeChanges: - docGenerationTask.mode = Mode.EDIT - this.enableUserInput(data.tabID) - break - case FollowUpTypes.AcceptChanges: - docGenerationTask.userDecision = 'ACCEPT' - await this.sendDocAcceptanceEvent(data) - await this.insertCode(data) - return - case FollowUpTypes.RejectChanges: - docGenerationTask.userDecision = 'REJECT' - await this.sendDocAcceptanceEvent(data) - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - disableChatInput: true, - message: 'Your changes have been discarded.', - followUps: NewSessionFollowUps, - }) - break - case FollowUpTypes.ProceedFolderSelection: - // If a user did not change the folder in a multi-root workspace, default to the first workspace folder - if (docGenerationTask.folderPath === '' && isMultiRootWorkspace()) { - docGenerationTask.folderPath = workspaceFolderName - } - if (docGenerationTask.mode === Mode.EDIT) { - this.enableUserInput(data.tabID) - } else { - await this.generateDocumentation( - { - ...data, - message: - docGenerationTask.mode === Mode.CREATE - ? 'Create documentation for a specific folder' - : 'Sync documentation', - }, - session, - docGenerationTask - ) - } - break - case FollowUpTypes.CancelFolderSelection: - docGenerationTask.reset() - return this.tabOpened(data) - } - }) - } - - private enableUserInput(tabID: string) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: tabID, - message: i18n('AWS.amazonq.doc.answer.editReadme'), - }) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.placeholder.editReadme')) - this.messenger.sendChatInputEnabled(tabID, true) - } - - private async fileClicked(message: any) { - const tabId: string = message.tabID - const messageId = message.messageId - const filePathToUpdate: string = message.filePath - - const session = await this.sessionStorage.getSession(tabId) - const filePathIndex = (session.state.filePaths ?? []).findIndex((obj) => obj.relativePath === filePathToUpdate) - if (filePathIndex !== -1 && session.state.filePaths) { - session.state.filePaths[filePathIndex].rejected = !session.state.filePaths[filePathIndex].rejected - } - const deletedFilePathIndex = (session.state.deletedFiles ?? []).findIndex( - (obj) => obj.relativePath === filePathToUpdate - ) - if (deletedFilePathIndex !== -1 && session.state.deletedFiles) { - session.state.deletedFiles[deletedFilePathIndex].rejected = - !session.state.deletedFiles[deletedFilePathIndex].rejected - } - - await session.updateFilesPaths( - tabId, - session.state.filePaths ?? [], - session.state.deletedFiles ?? [], - messageId, - true - ) - } - - private async formActionClicked(message: any) { - switch (message.action) { - case 'cancel-doc-generation': - // eslint-disable-next-line unicorn/no-null - await this.stopResponse(message) - - break - } - } - - private async newTask(message: any) { - // Old session for the tab is ending, delete it so we can create a new one for the message id - - this.docGenerationTasks.deleteTask(message.tabID) - this.sessionStorage.deleteSession(message.tabID) - - // Re-run the opening flow, where we check auth + create a session - await this.tabOpened(message) - } - - private async closeSession(message: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.answer.sessionClosed'), - disableChatInput: true, - }) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.sessionClosed')) - this.messenger.sendChatInputEnabled(message.tabID, false) - this.docGenerationTasks.deleteTask(message.tabID) - } - - private processErrorChatMessage = ( - err: any, - message: any, - session: Session | undefined, - docGenerationTask: DocGenerationTask - ) => { - const errorMessage = createUserFacingErrorMessage(`${err.cause?.message ?? err.message}`) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(message.tabID, null) - if (err.constructor.name === MonthlyConversationLimitError.name) { - this.messenger.sendMonthlyLimitError(message.tabID) - } else { - const enableUserInput = docGenerationTask.mode === Mode.EDIT && err.remainingIterations > 0 - - this.messenger.sendErrorMessage( - errorMessage, - message.tabID, - 0, - session?.conversationIdUnsafe, - false, - enableUserInput - ) - } - } - - private async generateDocumentation(message: any, session: any, docGenerationTask: DocGenerationTask) { - try { - await this.onDocsGeneration(session, message.message, message.tabID, docGenerationTask) - } catch (err: any) { - this.processErrorChatMessage(err, message, session, docGenerationTask) - } - } - - private async processUserChatMessage(message: any) { - if (message.message === undefined) { - this.messenger.sendErrorMessage('chatMessage should be set', message.tabID, 0, undefined) - return - } - - /** - * Don't attempt to process any chat messages when a workspace folder is not set. - * When the tab is first opened we will throw an error and lock the chat if the workspace - * folder is not found - */ - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - return - } - - const session: Session = await this.sessionStorage.getSession(message.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(message.tabID) - - try { - getLogger().debug(`${featureName}: Processing message: ${message.message}`) - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - - await this.generateDocumentation(message, session, docGenerationTask) - } catch (err: any) { - this.processErrorChatMessage(err, message, session, docGenerationTask) - } - } - - private async stopResponse(message: any) { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'), - type: 'answer-part', - tabID: message.tabID, - }) - // eslint-disable-next-line unicorn/no-null - this.messenger.sendUpdatePromptProgress(message.tabID, null) - this.messenger.sendChatInputEnabled(message.tabID, false) - - const session = await this.sessionStorage.getSession(message.tabID) - session.state.tokenSource?.cancel() - } - - private async tabOpened(message: any) { - let session: Session | undefined - try { - session = await this.sessionStorage.getSession(message.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(message.tabID) - getLogger().debug(`${featureName}: Session created with id: ${session.tabID}`) - docGenerationTask.folderPath = '' - docGenerationTask.mode = Mode.NONE - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - docGenerationTask.numberOfNavigations += 1 - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - followUps: [ - { - pillText: 'Create a README', - prompt: 'Create a README', - type: 'CreateDocumentation', - }, - { - pillText: 'Update an existing README', - prompt: 'Update an existing README', - type: 'UpdateDocumentation', - }, - ], - disableChatInput: true, - }) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) - } catch (err: any) { - if (err instanceof WorkspaceFolderNotFoundError) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - disableChatInput: true, - }) - } else { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(err.message), - message.tabID, - 0, - session?.conversationIdUnsafe - ) - } - } - } - - private async openMarkdownPreview(readmePath: vscode.Uri) { - await vscode.commands.executeCommand('vscode.open', readmePath) - await vscode.commands.executeCommand('markdown.showPreview') - } - - private async onDocsGeneration( - session: Session, - message: string, - tabID: string, - docGenerationTask: DocGenerationTask - ) { - this.messenger.sendDocProgress(tabID, DocGenerationStep.UPLOAD_TO_S3, 0, docGenerationTask.mode) - - await session.preloader(message) - - try { - await session.sendDocMetricData(MetricDataOperationName.StartDocGeneration, MetricDataResult.Success) - await session.send(message, docGenerationTask.mode, docGenerationTask.folderPath) - const filePaths = session.state.filePaths ?? [] - const deletedFiles = session.state.deletedFiles ?? [] - - // Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled - if (session?.state.tokenSource?.token.isCancellationRequested) { - return - } - - if (filePaths.length === 0 && deletedFiles.length === 0) { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.unableGenerateChanges'), - type: 'answer', - tabID: tabID, - canBeVoted: true, - disableChatInput: true, - }) - - return - } - - this.messenger.sendCodeResult( - filePaths, - deletedFiles, - session.state.references ?? [], - tabID, - session.uploadId, - session.state.codeGenerationId ?? '' - ) - - // Automatically open the README diff - const readmePath = findReadmePath(session.state.filePaths) - if (readmePath) { - await this.openDiff({ tabID, filePath: readmePath.zipFilePath }) - } - - const remainingIterations = session.state.codeGenerationRemainingIterationCount - const totalIterations = session.state.codeGenerationTotalIterationCount - - if (remainingIterations !== undefined && totalIterations !== undefined) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: tabID, - message: `${docGenerationTask.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${remainingIterations > 0 ? i18n('AWS.amazonq.doc.answer.codeResult') : i18n('AWS.amazonq.doc.answer.acceptOrReject')}`, - disableChatInput: true, - }) - - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - disableChatInput: true, - followUps: - remainingIterations > 0 - ? CodeChangeFollowUps - : CodeChangeFollowUps.filter((followUp) => followUp.type !== FollowUpTypes.MakeChanges), - tabID: tabID, - }) - } - if (session?.state.phase === DevPhase.CODEGEN) { - const docGenerationTask = this.docGenerationTasks.getTask(tabID) - const { totalGeneratedChars, totalGeneratedLines, totalGeneratedFiles } = - await session.countGeneratedContent(docGenerationTask.interactionType) - docGenerationTask.conversationId = session.conversationId - docGenerationTask.numberOfGeneratedChars = totalGeneratedChars - docGenerationTask.numberOfGeneratedLines = totalGeneratedLines - docGenerationTask.numberOfGeneratedFiles = totalGeneratedFiles - const docGenerationEvent = docGenerationTask.docGenerationEventBase() - - await session.sendDocTelemetryEvent(docGenerationEvent, 'generation') - } - } catch (err: any) { - getLogger().error(`${featureName}: Error during doc generation: ${err}`) - await session.sendDocMetricData(MetricDataOperationName.EndDocGeneration, getMetricResult(err)) - throw err - } finally { - if (session?.state?.tokenSource?.token.isCancellationRequested) { - await this.newTask({ tabID }) - } else { - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) - - this.messenger.sendChatInputEnabled(tabID, false) - } - } - await session.sendDocMetricData(MetricDataOperationName.EndDocGeneration, MetricDataResult.Success) - } - - private authClicked(message: any) { - this.authController.handleAuth(message.authType) - - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: 'Follow instructions to re-authenticate ...', - disableChatInput: true, - }) - } - - private tabClosed(message: any) { - this.sessionStorage.deleteSession(message.tabID) - this.docGenerationTasks.deleteTask(message.tabID) - } - - private async insertCode(message: any) { - let session - try { - session = await this.sessionStorage.getSession(message.tabID) - - await session.insertChanges() - - const readmePath = findReadmePath(session.state.filePaths) - if (readmePath) { - await this.openMarkdownPreview( - vscode.Uri.file(path.join(readmePath.workspaceFolder.uri.fsPath, readmePath.relativePath)) - ) - } - - this.messenger.sendAnswer({ - type: 'answer', - disableChatInput: true, - tabID: message.tabID, - followUps: NewSessionFollowUps, - }) - - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) - } catch (err: any) { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(`Failed to insert code changes: ${err.message}`), - message.tabID, - 0, - session?.conversationIdUnsafe - ) - } - } - private async sendDocAcceptanceEvent(message: any) { - const session = await this.sessionStorage.getSession(message.tabID) - const docGenerationTask = this.docGenerationTasks.getTask(message.tabID) - docGenerationTask.conversationId = session.conversationId - const { totalAddedChars, totalAddedLines, totalAddedFiles } = await session.countAddedContent( - docGenerationTask.interactionType - ) - docGenerationTask.numberOfAddedChars = totalAddedChars - docGenerationTask.numberOfAddedLines = totalAddedLines - docGenerationTask.numberOfAddedFiles = totalAddedFiles - const docAcceptanceEvent = docGenerationTask.docAcceptanceEventBase() - - await session.sendDocTelemetryEvent(docAcceptanceEvent, 'acceptance') - } - private processLink(message: any) { - void openUrl(vscode.Uri.parse(message.link)) - } -} diff --git a/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts b/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts deleted file mode 100644 index fe5dc25981c..00000000000 --- a/packages/core/src/amazonqDoc/controllers/docGenerationTask.ts +++ /dev/null @@ -1,100 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { - DocFolderLevel, - DocInteractionType, - DocUserDecision, - DocV2AcceptanceEvent, - DocV2GenerationEvent, -} from '../../codewhisperer/client/codewhispereruserclient' -import { getLogger } from '../../shared/logger/logger' -import { Mode } from '../constants' - -export class DocGenerationTasks { - private tasks: Map = new Map() - - public getTask(tabId: string): DocGenerationTask { - if (!this.tasks.has(tabId)) { - this.tasks.set(tabId, new DocGenerationTask()) - } - return this.tasks.get(tabId)! - } - - public deleteTask(tabId: string): void { - this.tasks.delete(tabId) - } -} - -export class DocGenerationTask { - public mode: Mode = Mode.NONE - public folderPath = '' - // Telemetry fields - public conversationId?: string - public numberOfAddedChars?: number - public numberOfAddedLines?: number - public numberOfAddedFiles?: number - public numberOfGeneratedChars?: number - public numberOfGeneratedLines?: number - public numberOfGeneratedFiles?: number - public userDecision?: DocUserDecision - public interactionType?: DocInteractionType - public numberOfNavigations = 0 - public folderLevel: DocFolderLevel = 'ENTIRE_WORKSPACE' - - public docGenerationEventBase() { - const undefinedProps = Object.entries(this) - .filter(([key, value]) => value === undefined) - .map(([key]) => key) - - if (undefinedProps.length > 0) { - getLogger().debug(`DocV2GenerationEvent has undefined properties: ${undefinedProps.join(', ')}`) - } - const event: DocV2GenerationEvent = { - conversationId: this.conversationId ?? '', - numberOfGeneratedChars: this.numberOfGeneratedChars ?? 0, - numberOfGeneratedLines: this.numberOfGeneratedLines ?? 0, - numberOfGeneratedFiles: this.numberOfGeneratedFiles ?? 0, - interactionType: this.interactionType, - numberOfNavigations: this.numberOfNavigations, - folderLevel: this.folderLevel, - } - return event - } - - public docAcceptanceEventBase() { - const undefinedProps = Object.entries(this) - .filter(([key, value]) => value === undefined) - .map(([key]) => key) - - if (undefinedProps.length > 0) { - getLogger().debug(`DocV2AcceptanceEvent has undefined properties: ${undefinedProps.join(', ')}`) - } - const event: DocV2AcceptanceEvent = { - conversationId: this.conversationId ?? '', - numberOfAddedChars: this.numberOfAddedChars ?? 0, - numberOfAddedLines: this.numberOfAddedLines ?? 0, - numberOfAddedFiles: this.numberOfAddedFiles ?? 0, - userDecision: this.userDecision ?? 'ACCEPTED', - interactionType: this.interactionType ?? 'GENERATE_README', - numberOfNavigations: this.numberOfNavigations ?? 0, - folderLevel: this.folderLevel, - } - return event - } - - public reset() { - this.conversationId = undefined - this.numberOfAddedChars = undefined - this.numberOfAddedLines = undefined - this.numberOfAddedFiles = undefined - this.numberOfGeneratedChars = undefined - this.numberOfGeneratedLines = undefined - this.numberOfGeneratedFiles = undefined - this.userDecision = undefined - this.interactionType = undefined - this.numberOfNavigations = 0 - this.folderLevel = 'ENTIRE_WORKSPACE' - } -} diff --git a/packages/core/src/amazonqDoc/errors.ts b/packages/core/src/amazonqDoc/errors.ts deleted file mode 100644 index fb1ffa033c2..00000000000 --- a/packages/core/src/amazonqDoc/errors.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ClientError, ContentLengthError as CommonContentLengthError } from '../shared/errors' -import { i18n } from '../shared/i18n-helper' - -export class DocClientError extends ClientError { - remainingIterations?: number - constructor(message: string, code: string, remainingIterations?: number) { - super(message, { code }) - this.remainingIterations = remainingIterations - } -} - -export class ReadmeTooLargeError extends DocClientError { - constructor() { - super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), ReadmeTooLargeError.name) - } -} - -export class ReadmeUpdateTooLargeError extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), ReadmeUpdateTooLargeError.name, remainingIterations) - } -} - -export class WorkspaceEmptyError extends DocClientError { - constructor() { - super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), WorkspaceEmptyError.name) - } -} - -export class NoChangeRequiredException extends DocClientError { - constructor() { - super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), NoChangeRequiredException.name) - } -} - -export class PromptRefusalException extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.promptRefusal'), PromptRefusalException.name, remainingIterations) - } -} - -export class ContentLengthError extends CommonContentLengthError { - constructor() { - super(i18n('AWS.amazonq.doc.error.contentLengthError'), { code: ContentLengthError.name }) - } -} - -export class PromptTooVagueError extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.promptTooVague'), PromptTooVagueError.name, remainingIterations) - } -} - -export class PromptUnrelatedError extends DocClientError { - constructor(remainingIterations: number) { - super(i18n('AWS.amazonq.doc.error.promptUnrelated'), PromptUnrelatedError.name, remainingIterations) - } -} diff --git a/packages/core/src/amazonqDoc/index.ts b/packages/core/src/amazonqDoc/index.ts deleted file mode 100644 index 7ba22e4b351..00000000000 --- a/packages/core/src/amazonqDoc/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './types' -export * from './session/sessionState' -export * from './constants' -export { Session } from './session/session' -export { ChatControllerEventEmitters, DocController } from './controllers/chat/controller' diff --git a/packages/core/src/amazonqDoc/messenger.ts b/packages/core/src/amazonqDoc/messenger.ts deleted file mode 100644 index 3c6abfdf15f..00000000000 --- a/packages/core/src/amazonqDoc/messenger.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { Messenger } from '../amazonq/commons/connector/baseMessenger' -import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' -import { messageWithConversationId } from '../amazonqFeatureDev/userFacingText' -import { i18n } from '../shared/i18n-helper' -import { docGenerationProgressMessage, DocGenerationStep, Mode, NewSessionFollowUps } from './constants' -import { inProgress } from './types' - -export class DocMessenger extends Messenger { - public constructor(dispatcher: AppToWebViewMessageDispatcher, sender: string) { - super(dispatcher, sender) - } - - /** Sends a message in the chat and displays a prompt input progress bar to communicate the doc generation progress. - * The text in the progress bar matches the current step shown in the message. - * - */ - public sendDocProgress(tabID: string, step: DocGenerationStep, progress: number, mode: Mode) { - // Hide prompt input progress bar once all steps are completed - if (step > DocGenerationStep.GENERATING_ARTIFACTS) { - // eslint-disable-next-line unicorn/no-null - this.sendUpdatePromptProgress(tabID, null) - } else { - const progressText = - step === DocGenerationStep.UPLOAD_TO_S3 - ? `${i18n('AWS.amazonq.doc.answer.scanning')}...` - : step === DocGenerationStep.SUMMARIZING_FILES - ? `${i18n('AWS.amazonq.doc.answer.summarizing')}...` - : `${i18n('AWS.amazonq.doc.answer.generating')}...` - this.sendUpdatePromptProgress(tabID, inProgress(progress, progressText)) - } - - // The first step is answer-stream type, subequent updates are answer-part - this.sendAnswer({ - type: step === DocGenerationStep.UPLOAD_TO_S3 ? 'answer-stream' : 'answer-part', - tabID: tabID, - disableChatInput: true, - message: docGenerationProgressMessage(step, mode), - }) - } - - public override sendErrorMessage( - errorMessage: string, - tabID: string, - _retries: number, - conversationId?: string, - _showDefaultMessage?: boolean, - enableUserInput?: boolean - ) { - if (enableUserInput) { - this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.placeholder.editReadme')) - this.sendChatInputEnabled(tabID, true) - } - this.sendAnswer({ - type: 'answer', - tabID: tabID, - message: errorMessage + messageWithConversationId(conversationId), - followUps: enableUserInput ? [] : NewSessionFollowUps, - disableChatInput: !enableUserInput, - }) - } -} diff --git a/packages/core/src/amazonqDoc/session/session.ts b/packages/core/src/amazonqDoc/session/session.ts deleted file mode 100644 index e3eb29d6d32..00000000000 --- a/packages/core/src/amazonqDoc/session/session.ts +++ /dev/null @@ -1,371 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { docScheme, featureName, Mode } from '../constants' -import { DeletedFileInfo, Interaction, NewFileInfo, SessionState, SessionStateConfig } from '../types' -import { DocPrepareCodeGenState } from './sessionState' -import { telemetry } from '../../shared/telemetry/telemetry' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { SessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import path from 'path' -import { FeatureDevClient } from '../../amazonqFeatureDev/client/featureDev' -import { TelemetryHelper } from '../../amazonq/util/telemetryHelper' -import { ConversationNotStartedState } from '../../amazonqFeatureDev/session/sessionState' -import { logWithConversationId } from '../../amazonqFeatureDev/userFacingText' -import { ConversationIdNotFoundError, IllegalStateError } from '../../amazonqFeatureDev/errors' -import { referenceLogText } from '../../amazonq/commons/model' -import { - DocInteractionType, - DocV2AcceptanceEvent, - DocV2GenerationEvent, - SendTelemetryEventRequest, - MetricData, -} from '../../codewhisperer/client/codewhispereruserclient' -import { getClientId, getOperatingSystem, getOptOutPreference } from '../../shared/telemetry/util' -import { DocMessenger } from '../messenger' -import { computeDiff } from '../../amazonq/commons/diff' -import { ReferenceLogViewProvider } from '../../codewhisperer/service/referenceLogViewProvider' -import fs from '../../shared/fs/fs' -import globals from '../../shared/extensionGlobals' -import { extensionVersion } from '../../shared/vscode/env' -import { getLogger } from '../../shared/logger/logger' -import { ContentLengthError as CommonContentLengthError } from '../../shared/errors' -import { ContentLengthError } from '../errors' - -export class Session { - private _state?: SessionState | Omit - private task: string = '' - private proxyClient: FeatureDevClient - private _conversationId?: string - private preloaderFinished = false - private _latestMessage: string = '' - private _telemetry: TelemetryHelper - - // Used to keep track of whether or not the current session is currently authenticating/needs authenticating - public isAuthenticating: boolean - private _reportedDocChanges: { [key: string]: string } = {} - - constructor( - public readonly config: SessionConfig, - private messenger: DocMessenger, - public readonly tabID: string, - initialState: Omit = new ConversationNotStartedState(tabID), - proxyClient: FeatureDevClient = new FeatureDevClient() - ) { - this._state = initialState - this.proxyClient = proxyClient - - this._telemetry = new TelemetryHelper() - this.isAuthenticating = false - } - - /** - * Preload any events that have to run before a chat message can be sent - */ - async preloader(msg: string) { - if (!this.preloaderFinished) { - await this.setupConversation(msg) - this.preloaderFinished = true - } - } - - get state() { - if (!this._state) { - throw new IllegalStateError("State should be initialized before it's read") - } - return this._state - } - - /** - * setupConversation - * - * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it. - */ - private async setupConversation(msg: string) { - // Store the initial message when setting up the conversation so that if it fails we can retry with this message - this._latestMessage = msg - - await telemetry.amazonq_startConversationInvoke.run(async (span) => { - this._conversationId = await this.proxyClient.createConversation() - getLogger().info(logWithConversationId(this.conversationId)) - - span.record({ amazonqConversationId: this._conversationId, credentialStartUrl: AuthUtil.instance.startUrl }) - }) - - this._state = new DocPrepareCodeGenState( - { - ...this.getSessionStateConfig(), - conversationId: this.conversationId, - uploadId: '', - currentCodeGenerationId: undefined, - }, - [], - [], - [], - this.tabID, - 0 - ) - } - - private getSessionStateConfig(): Omit { - return { - workspaceRoots: this.config.workspaceRoots, - workspaceFolders: this.config.workspaceFolders, - proxyClient: this.proxyClient, - conversationId: this.conversationId, - } - } - - async send(msg: string, mode: Mode, folderPath?: string): Promise { - // When the task/"thing to do" hasn't been set yet, we want it to be the incoming message - if (this.task === '' && msg) { - this.task = msg - } - - this._latestMessage = msg - - return this.nextInteraction(msg, mode, folderPath) - } - private async nextInteraction(msg: string, mode: Mode, folderPath?: string) { - try { - const resp = await this.state.interact({ - task: this.task, - msg, - fs: this.config.fs, - mode: mode, - folderPath: folderPath, - messenger: this.messenger, - telemetry: this.telemetry, - tokenSource: this.state.tokenSource, - uploadHistory: this.state.uploadHistory, - }) - - if (resp.nextState) { - if (!this.state?.tokenSource?.token.isCancellationRequested) { - this.state?.tokenSource?.cancel() - } - - // Move to the next state - this._state = resp.nextState - } - - return resp.interaction - } catch (e) { - if (e instanceof CommonContentLengthError) { - getLogger().debug(`Content length validation failed: ${e.message}`) - throw new ContentLengthError() - } - throw e - } - } - - public async updateFilesPaths( - tabID: string, - filePaths: NewFileInfo[], - deletedFiles: DeletedFileInfo[], - messageId: string, - disableFileActions: boolean - ) { - this.messenger.updateFileComponent(tabID, filePaths, deletedFiles, messageId, disableFileActions) - } - - public async insertChanges() { - for (const filePath of this.state.filePaths?.filter((i) => !i.rejected) ?? []) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - - const uri = filePath.virtualMemoryUri - const content = await this.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - - await fs.mkdir(path.dirname(absolutePath)) - await fs.writeFile(absolutePath, decodedContent) - } - - for (const filePath of this.state.deletedFiles?.filter((i) => !i.rejected) ?? []) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - await fs.delete(absolutePath) - } - - for (const ref of this.state.references ?? []) { - ReferenceLogViewProvider.instance.addReferenceLog(referenceLogText(ref)) - } - } - - private getFromReportedChanges(filepath: NewFileInfo) { - const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}` - return this._reportedDocChanges[key] - } - - private addToReportedChanges(filepath: NewFileInfo) { - const key = `${filepath.workspaceFolder.uri.fsPath}/${filepath.relativePath}` - this._reportedDocChanges[key] = filepath.fileContent - } - - public async countGeneratedContent(interactionType?: DocInteractionType) { - let totalGeneratedChars = 0 - let totalGeneratedLines = 0 - let totalGeneratedFiles = 0 - const filePaths = this.state.filePaths ?? [] - - for (const filePath of filePaths) { - const reportedDocChange = this.getFromReportedChanges(filePath) - if (interactionType === 'GENERATE_README') { - if (reportedDocChange) { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange) - totalGeneratedChars += charsAdded - totalGeneratedLines += linesAdded - } else { - // If no changes are reported, this is the initial README generation and no comparison with existing files is needed - const fileContent = filePath.fileContent - totalGeneratedChars += fileContent.length - totalGeneratedLines += fileContent.split('\n').length - } - } else { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath, reportedDocChange) - totalGeneratedChars += charsAdded - totalGeneratedLines += linesAdded - } - this.addToReportedChanges(filePath) - totalGeneratedFiles += 1 - } - return { - totalGeneratedChars, - totalGeneratedLines, - totalGeneratedFiles, - } - } - - public async countAddedContent(interactionType?: DocInteractionType) { - let totalAddedChars = 0 - let totalAddedLines = 0 - let totalAddedFiles = 0 - const newFilePaths = - this.state.filePaths?.filter((filePath) => !filePath.rejected && !filePath.changeApplied) ?? [] - - for (const filePath of newFilePaths) { - if (interactionType === 'GENERATE_README') { - const fileContent = filePath.fileContent - totalAddedChars += fileContent.length - totalAddedLines += fileContent.split('\n').length - } else { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath) - totalAddedChars += charsAdded - totalAddedLines += linesAdded - } - totalAddedFiles += 1 - } - return { - totalAddedChars, - totalAddedLines, - totalAddedFiles, - } - } - - public async computeFilePathDiff(filePath: NewFileInfo, reportedChanges?: string) { - const leftPath = `${filePath.workspaceFolder.uri.fsPath}/${filePath.relativePath}` - const rightPath = filePath.virtualMemoryUri.path - const diff = await computeDiff(leftPath, rightPath, this.tabID, docScheme, reportedChanges) - return { leftPath, rightPath, ...diff } - } - - public async sendDocMetricData(operationName: string, result: string) { - const metricData = { - metricName: 'Operation', - metricValue: 1, - timestamp: new Date(), - product: 'DocGeneration', - dimensions: [ - { - name: 'operationName', - value: operationName, - }, - { - name: 'result', - value: result, - }, - ], - } - await this.sendDocTelemetryEvent(metricData, 'metric') - } - - public async sendDocTelemetryEvent( - telemetryEvent: DocV2GenerationEvent | DocV2AcceptanceEvent | MetricData, - eventType: 'generation' | 'acceptance' | 'metric' - ) { - const client = await this.proxyClient.getClient() - const telemetryEventKey = { - generation: 'docV2GenerationEvent', - acceptance: 'docV2AcceptanceEvent', - metric: 'metricData', - }[eventType] - try { - const params: SendTelemetryEventRequest = { - telemetryEvent: { - [telemetryEventKey]: telemetryEvent, - }, - optOutPreference: getOptOutPreference(), - userContext: { - ideCategory: 'VSCODE', - operatingSystem: getOperatingSystem(), - product: 'DocGeneration', // Should be the same as in JetBrains - clientId: getClientId(globals.globalState), - ideVersion: extensionVersion, - }, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - - const response = await client.sendTelemetryEvent(params).promise() - if (eventType === 'metric') { - getLogger().debug( - `${featureName}: successfully sent metricData: RequestId: ${response.$response.requestId}` - ) - } else { - getLogger().debug( - `${featureName}: successfully sent docV2${eventType === 'generation' ? 'GenerationEvent' : 'AcceptanceEvent'}: ` + - `ConversationId: ${(telemetryEvent as DocV2GenerationEvent | DocV2AcceptanceEvent).conversationId} ` + - `RequestId: ${response.$response.requestId}` - ) - } - } catch (e) { - const error = e as Error - const eventTypeString = eventType === 'metric' ? 'metricData' : `doc ${eventType}` - getLogger().error( - `${featureName}: failed to send ${eventTypeString} telemetry: ${error.name}: ${error.message} ` + - `RequestId: ${(e as any).$response?.requestId}` - ) - } - } - - get currentCodeGenerationId() { - return this.state.currentCodeGenerationId - } - - get uploadId() { - if (!('uploadId' in this.state)) { - throw new IllegalStateError("UploadId has to be initialized before it's read") - } - return this.state.uploadId - } - - get conversationId() { - if (!this._conversationId) { - throw new ConversationIdNotFoundError() - } - return this._conversationId - } - - // Used for cases where it is not needed to have conversationId - get conversationIdUnsafe() { - return this._conversationId - } - - get latestMessage() { - return this._latestMessage - } - - get telemetry() { - return this._telemetry - } -} diff --git a/packages/core/src/amazonqDoc/session/sessionState.ts b/packages/core/src/amazonqDoc/session/sessionState.ts deleted file mode 100644 index e95bc48f772..00000000000 --- a/packages/core/src/amazonqDoc/session/sessionState.ts +++ /dev/null @@ -1,165 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DocGenerationStep, docScheme, getFileSummaryPercentage, Mode } from '../constants' - -import { i18n } from '../../shared/i18n-helper' - -import { CurrentWsFolders, NewFileInfo, SessionState, SessionStateAction, SessionStateConfig } from '../types' -import { - ContentLengthError, - NoChangeRequiredException, - PromptRefusalException, - PromptTooVagueError, - PromptUnrelatedError, - ReadmeTooLargeError, - ReadmeUpdateTooLargeError, - WorkspaceEmptyError, -} from '../errors' -import { ApiClientError, ApiServiceError } from '../../amazonqFeatureDev/errors' -import { DocMessenger } from '../messenger' -import { BaseCodeGenState, BasePrepareCodeGenState, CreateNextStateParams } from '../../amazonq/session/sessionState' -import { Intent } from '../../amazonq/commons/types' -import { AmazonqCreateUpload, Span } from '../../shared/telemetry/telemetry' -import { prepareRepoData, PrepareRepoDataOptions } from '../../amazonq/util/files' -import { LlmError } from '../../amazonq/errors' - -export class DocCodeGenState extends BaseCodeGenState { - protected handleProgress(messenger: DocMessenger, action: SessionStateAction, detail?: string): void { - if (detail) { - const progress = getFileSummaryPercentage(detail) - messenger.sendDocProgress( - this.tabID, - progress === 100 ? DocGenerationStep.GENERATING_ARTIFACTS : DocGenerationStep.SUMMARIZING_FILES, - progress, - action.mode - ) - } - } - - protected getScheme(): string { - return docScheme - } - - protected getTimeoutErrorCode(): string { - return 'DocGenerationTimeout' - } - - protected handleGenerationComplete( - messenger: DocMessenger, - newFileInfo: NewFileInfo[], - action: SessionStateAction - ): void { - messenger.sendDocProgress(this.tabID, DocGenerationStep.GENERATING_ARTIFACTS + 1, 100, action.mode) - } - - protected handleError(messenger: DocMessenger, codegenResult: any): Error { - // eslint-disable-next-line unicorn/no-null - messenger.sendUpdatePromptProgress(this.tabID, null) - - switch (true) { - case codegenResult.codeGenerationStatusDetail?.includes('README_TOO_LARGE'): { - return new ReadmeTooLargeError() - } - case codegenResult.codeGenerationStatusDetail?.includes('README_UPDATE_TOO_LARGE'): { - return new ReadmeUpdateTooLargeError(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('WORKSPACE_TOO_LARGE'): { - return new ContentLengthError() - } - case codegenResult.codeGenerationStatusDetail?.includes('WORKSPACE_EMPTY'): { - return new WorkspaceEmptyError() - } - case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_UNRELATED'): { - return new PromptUnrelatedError(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_TOO_VAGUE'): { - return new PromptTooVagueError(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_REFUSAL'): { - return new PromptRefusalException(codegenResult.codeGenerationRemainingIterationCount || 0) - } - case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { - return new ApiClientError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'GuardrailsException', - 400 - ) - } - case codegenResult.codeGenerationStatusDetail?.includes('EmptyPatch'): { - if (codegenResult.codeGenerationStatusDetail?.includes('NO_CHANGE_REQUIRED')) { - return new NoChangeRequiredException() - } - - return new LlmError(i18n('AWS.amazonq.doc.error.docGen.default'), { - code: 'EmptyPatchException', - }) - } - case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { - return new ApiClientError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'GetTaskAssistCodeGeneration', - 'ThrottlingException', - 429 - ) - } - default: { - return new ApiServiceError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'UnknownException', - 500 - ) - } - } - } - - protected async startCodeGeneration(action: SessionStateAction, codeGenerationId: string): Promise { - if (!action.tokenSource?.token.isCancellationRequested) { - action.messenger.sendDocProgress(this.tabID, DocGenerationStep.SUMMARIZING_FILES, 0, action.mode as Mode) - } - - await this.config.proxyClient.startCodeGeneration( - this.config.conversationId, - this.config.uploadId, - action.msg, - Intent.DOC, - codeGenerationId, - undefined, - action.folderPath ? { documentation: { type: 'README', scope: action.folderPath } } : undefined - ) - } - - protected override createNextState(config: SessionStateConfig, params: CreateNextStateParams): SessionState { - return super.createNextState(config, params, DocPrepareCodeGenState) - } -} - -export class DocPrepareCodeGenState extends BasePrepareCodeGenState { - protected preUpload(action: SessionStateAction): void { - // Do nothing - } - - protected postUpload(action: SessionStateAction): void { - // Do nothing - } - - protected override createNextState(config: SessionStateConfig): SessionState { - return super.createNextState(config, DocCodeGenState) - } - - protected override async prepareProjectZip( - workspaceRoots: string[], - workspaceFolders: CurrentWsFolders, - span: Span, - options: PrepareRepoDataOptions - ) { - return await prepareRepoData(workspaceRoots, workspaceFolders, span, { - ...options, - isIncludeInfraDiagram: true, - }) - } -} diff --git a/packages/core/src/amazonqDoc/storages/chatSession.ts b/packages/core/src/amazonqDoc/storages/chatSession.ts deleted file mode 100644 index 34fb9f5404e..00000000000 --- a/packages/core/src/amazonqDoc/storages/chatSession.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { BaseChatSessionStorage } from '../../amazonq/commons/baseChatStorage' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { docScheme } from '../constants' -import { DocMessenger } from '../messenger' -import { Session } from '../session/session' - -export class DocChatSessionStorage extends BaseChatSessionStorage { - constructor(protected readonly messenger: DocMessenger) { - super() - } - - override async createSession(tabID: string): Promise { - const sessionConfig = await createSessionConfig(docScheme) - const session = new Session(sessionConfig, this.messenger, tabID) - this.sessions.set(tabID, session) - return session - } -} diff --git a/packages/core/src/amazonqDoc/types.ts b/packages/core/src/amazonqDoc/types.ts deleted file mode 100644 index 005353d0e23..00000000000 --- a/packages/core/src/amazonqDoc/types.ts +++ /dev/null @@ -1,83 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemButton, MynahIcons, ProgressField } from '@aws/mynah-ui' -import { - LLMResponseType, - SessionStorage, - SessionInfo, - DeletedFileInfo, - NewFileInfo, - NewFileZipContents, - SessionStateConfig, - SessionStatePhase, - DevPhase, - Interaction, - CurrentWsFolders, - CodeGenerationStatus, - SessionState as FeatureDevSessionState, - SessionStateAction as FeatureDevSessionStateAction, - SessionStateInteraction as FeatureDevSessionStateInteraction, -} from '../amazonq/commons/types' - -import { Mode } from './constants' -import { DocMessenger } from './messenger' - -export const cancelDocGenButton: ChatItemButton = { - id: 'cancel-doc-generation', - text: 'Cancel', - icon: 'cancel' as MynahIcons, -} - -export const inProgress = (progress: number, text: string): ProgressField => { - return { - status: 'default', - text, - value: progress === 100 ? -1 : progress, - actions: [cancelDocGenButton], - } -} - -export interface SessionStateInteraction extends FeatureDevSessionStateInteraction { - nextState: SessionState | Omit | undefined - interaction: Interaction -} - -export interface SessionState extends FeatureDevSessionState { - interact(action: SessionStateAction): Promise -} - -export interface SessionStateAction extends FeatureDevSessionStateAction { - messenger: DocMessenger - mode: Mode - folderPath?: string -} - -export enum MetricDataOperationName { - StartDocGeneration = 'StartDocGeneration', - EndDocGeneration = 'EndDocGeneration', -} - -export enum MetricDataResult { - Success = 'Success', - Fault = 'Fault', - Error = 'Error', - LlmFailure = 'LLMFailure', -} - -export { - LLMResponseType, - SessionStorage, - SessionInfo, - DeletedFileInfo, - NewFileInfo, - NewFileZipContents, - SessionStateConfig, - SessionStatePhase, - DevPhase, - Interaction, - CodeGenerationStatus, - CurrentWsFolders, -} diff --git a/packages/core/src/amazonqDoc/views/actions/uiMessageListener.ts b/packages/core/src/amazonqDoc/views/actions/uiMessageListener.ts deleted file mode 100644 index c6960b15fcc..00000000000 --- a/packages/core/src/amazonqDoc/views/actions/uiMessageListener.ts +++ /dev/null @@ -1,168 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatControllerEventEmitters } from '../../controllers/chat/controller' -import { MessageListener } from '../../../amazonq/messages/messageListener' -import { ExtensionMessage } from '../../../amazonq/webview/ui/commands' - -export interface UIMessageListenerProps { - readonly chatControllerEventEmitters: ChatControllerEventEmitters - readonly webViewMessageListener: MessageListener -} - -export class UIMessageListener { - private docGenerationControllerEventsEmitters: ChatControllerEventEmitters | undefined - private webViewMessageListener: MessageListener - - constructor(props: UIMessageListenerProps) { - this.docGenerationControllerEventsEmitters = props.chatControllerEventEmitters - this.webViewMessageListener = props.webViewMessageListener - - // Now we are listening to events that get sent from amazonq/webview/actions/actionListener (e.g. the tab) - this.webViewMessageListener.onMessage((msg) => { - this.handleMessage(msg) - }) - } - - private handleMessage(msg: ExtensionMessage) { - switch (msg.command) { - case 'chat-prompt': - this.processChatMessage(msg) - break - case 'follow-up-was-clicked': - this.followUpClicked(msg) - break - case 'open-diff': - this.openDiff(msg) - break - case 'chat-item-voted': - this.chatItemVoted(msg) - break - case 'chat-item-feedback': - this.chatItemFeedback(msg) - break - case 'stop-response': - this.stopResponse(msg) - break - case 'new-tab-was-created': - this.tabOpened(msg) - break - case 'tab-was-removed': - this.tabClosed(msg) - break - case 'auth-follow-up-was-clicked': - this.authClicked(msg) - break - case 'response-body-link-click': - this.processResponseBodyLinkClick(msg) - break - case 'insert_code_at_cursor_position': - this.insertCodeAtPosition(msg) - break - case 'file-click': - this.fileClicked(msg) - break - case 'form-action-click': - this.formActionClicked(msg) - break - } - } - - private chatItemVoted(msg: any) { - this.docGenerationControllerEventsEmitters?.processChatItemVotedMessage.fire({ - tabID: msg.tabID, - command: msg.command, - vote: msg.vote, - messageId: msg.messageId, - }) - } - - private chatItemFeedback(msg: any) { - this.docGenerationControllerEventsEmitters?.processChatItemFeedbackMessage.fire(msg) - } - - private processChatMessage(msg: any) { - this.docGenerationControllerEventsEmitters?.processHumanChatMessage.fire({ - message: msg.chatMessage, - tabID: msg.tabID, - }) - } - - private followUpClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.followUpClicked.fire({ - followUp: msg.followUp, - tabID: msg.tabID, - }) - } - - private formActionClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.formActionClicked.fire({ - ...msg, - }) - } - - private fileClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.fileClicked.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - actionName: msg.actionName, - messageId: msg.messageId, - }) - } - - private openDiff(msg: any) { - this.docGenerationControllerEventsEmitters?.openDiff.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - deleted: msg.deleted, - messageId: msg.messageId, - }) - } - - private stopResponse(msg: any) { - this.docGenerationControllerEventsEmitters?.stopResponse.fire({ - tabID: msg.tabID, - }) - } - - private tabOpened(msg: any) { - this.docGenerationControllerEventsEmitters?.tabOpened.fire({ - tabID: msg.tabID, - }) - } - - private tabClosed(msg: any) { - this.docGenerationControllerEventsEmitters?.tabClosed.fire({ - tabID: msg.tabID, - }) - } - - private authClicked(msg: any) { - this.docGenerationControllerEventsEmitters?.authClicked.fire({ - tabID: msg.tabID, - authType: msg.authType, - }) - } - - private processResponseBodyLinkClick(msg: any) { - this.docGenerationControllerEventsEmitters?.processResponseBodyLinkClick.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - link: msg.link, - }) - } - - private insertCodeAtPosition(msg: any) { - this.docGenerationControllerEventsEmitters?.insertCodeAtPositionClicked.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - code: msg.code, - insertionTargetType: msg.insertionTargetType, - codeReference: msg.codeReference, - }) - } -} diff --git a/packages/core/src/amazonqFeatureDev/app.ts b/packages/core/src/amazonqFeatureDev/app.ts deleted file mode 100644 index fd0652fd0e4..00000000000 --- a/packages/core/src/amazonqFeatureDev/app.ts +++ /dev/null @@ -1,106 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import { UIMessageListener } from './views/actions/uiMessageListener' -import { ChatControllerEventEmitters, FeatureDevController } from './controllers/chat/controller' -import { AmazonQAppInitContext } from '../amazonq/apps/initContext' -import { MessageListener } from '../amazonq/messages/messageListener' -import { fromQueryToParameters } from '../shared/utilities/uriUtils' -import { getLogger } from '../shared/logger/logger' -import { TabIdNotFoundError } from './errors' -import { featureDevChat, featureDevScheme } from './constants' -import globals from '../shared/extensionGlobals' -import { FeatureDevChatSessionStorage } from './storages/chatSession' -import { AuthUtil } from '../codewhisperer/util/authUtil' -import { debounce } from 'lodash' -import { Messenger } from '../amazonq/commons/connector/baseMessenger' -import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' - -export function init(appContext: AmazonQAppInitContext) { - const featureDevChatControllerEventEmitters: ChatControllerEventEmitters = { - processHumanChatMessage: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - stopResponse: new vscode.EventEmitter(), - tabOpened: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - insertCodeAtPositionClicked: new vscode.EventEmitter(), - fileClicked: new vscode.EventEmitter(), - storeCodeResultMessageId: new vscode.EventEmitter(), - } - - const messenger = new Messenger( - new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher()), - featureDevChat - ) - const sessionStorage = new FeatureDevChatSessionStorage(messenger) - - new FeatureDevController( - featureDevChatControllerEventEmitters, - messenger, - sessionStorage, - appContext.onDidChangeAmazonQVisibility.event - ) - - const featureDevProvider = new (class implements vscode.TextDocumentContentProvider { - async provideTextDocumentContent(uri: vscode.Uri): Promise { - const params = fromQueryToParameters(uri.query) - - const tabID = params.get('tabID') - if (!tabID) { - getLogger().error(`Unable to find tabID from ${uri.toString()}`) - throw new TabIdNotFoundError() - } - - const session = await sessionStorage.getSession(tabID) - const content = await session.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - return decodedContent - } - })() - - const textDocumentProvider = vscode.workspace.registerTextDocumentContentProvider( - featureDevScheme, - featureDevProvider - ) - - globals.context.subscriptions.push(textDocumentProvider) - - const featureDevChatUIInputEventEmitter = new vscode.EventEmitter() - - new UIMessageListener({ - chatControllerEventEmitters: featureDevChatControllerEventEmitters, - webViewMessageListener: new MessageListener(featureDevChatUIInputEventEmitter), - }) - - const debouncedEvent = debounce(async () => { - const authenticated = (await AuthUtil.instance.getChatAuthState()).amazonQ === 'connected' - let authenticatingSessionIDs: string[] = [] - if (authenticated) { - const authenticatingSessions = sessionStorage.getAuthenticatingSessions() - - authenticatingSessionIDs = authenticatingSessions.map((session) => session.tabID) - - // We've already authenticated these sessions - for (const session of authenticatingSessions) { - session.isAuthenticating = false - } - } - - messenger.sendAuthenticationUpdate(authenticated, authenticatingSessionIDs) - }, 500) - - AuthUtil.instance.secondaryAuth.onDidChangeActiveConnection(() => { - return debouncedEvent() - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - return debouncedEvent() - }) -} diff --git a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json b/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json deleted file mode 100644 index 812bbd4fd69..00000000000 --- a/packages/core/src/amazonqFeatureDev/client/codewhispererruntime-2022-11-11.json +++ /dev/null @@ -1,5640 +0,0 @@ -{ - "version": "2.0", - "metadata": { - "apiVersion": "2022-11-11", - "auth": ["smithy.api#httpBearerAuth"], - "endpointPrefix": "amazoncodewhispererservice", - "jsonVersion": "1.0", - "protocol": "json", - "protocols": ["json"], - "serviceFullName": "Amazon CodeWhisperer", - "serviceId": "CodeWhispererRuntime", - "signatureVersion": "bearer", - "signingName": "amazoncodewhispererservice", - "targetPrefix": "AmazonCodeWhispererService", - "uid": "codewhispererruntime-2022-11-11" - }, - "operations": { - "CreateArtifactUploadUrl": { - "name": "CreateArtifactUploadUrl", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateUploadUrlRequest" - }, - "output": { - "shape": "CreateUploadUrlResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "CreateTaskAssistConversation": { - "name": "CreateTaskAssistConversation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateTaskAssistConversationRequest" - }, - "output": { - "shape": "CreateTaskAssistConversationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ServiceQuotaExceededException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "CreateUploadUrl": { - "name": "CreateUploadUrl", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateUploadUrlRequest" - }, - "output": { - "shape": "CreateUploadUrlResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ServiceQuotaExceededException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "CreateUserMemoryEntry": { - "name": "CreateUserMemoryEntry", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateUserMemoryEntryInput" - }, - "output": { - "shape": "CreateUserMemoryEntryOutput" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "CreateWorkspace": { - "name": "CreateWorkspace", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "CreateWorkspaceRequest" - }, - "output": { - "shape": "CreateWorkspaceResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "DeleteTaskAssistConversation": { - "name": "DeleteTaskAssistConversation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "DeleteTaskAssistConversationRequest" - }, - "output": { - "shape": "DeleteTaskAssistConversationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "DeleteUserMemoryEntry": { - "name": "DeleteUserMemoryEntry", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "DeleteUserMemoryEntryInput" - }, - "output": { - "shape": "DeleteUserMemoryEntryOutput" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "DeleteWorkspace": { - "name": "DeleteWorkspace", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "DeleteWorkspaceRequest" - }, - "output": { - "shape": "DeleteWorkspaceResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GenerateCompletions": { - "name": "GenerateCompletions", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GenerateCompletionsRequest" - }, - "output": { - "shape": "GenerateCompletionsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetCodeAnalysis": { - "name": "GetCodeAnalysis", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetCodeAnalysisRequest" - }, - "output": { - "shape": "GetCodeAnalysisResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetCodeFixJob": { - "name": "GetCodeFixJob", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetCodeFixJobRequest" - }, - "output": { - "shape": "GetCodeFixJobResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTaskAssistCodeGeneration": { - "name": "GetTaskAssistCodeGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTaskAssistCodeGenerationRequest" - }, - "output": { - "shape": "GetTaskAssistCodeGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTestGeneration": { - "name": "GetTestGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTestGenerationRequest" - }, - "output": { - "shape": "GetTestGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTransformation": { - "name": "GetTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTransformationRequest" - }, - "output": { - "shape": "GetTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "GetTransformationPlan": { - "name": "GetTransformationPlan", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "GetTransformationPlanRequest" - }, - "output": { - "shape": "GetTransformationPlanResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListAvailableCustomizations": { - "name": "ListAvailableCustomizations", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListAvailableCustomizationsRequest" - }, - "output": { - "shape": "ListAvailableCustomizationsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListAvailableProfiles": { - "name": "ListAvailableProfiles", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListAvailableProfilesRequest" - }, - "output": { - "shape": "ListAvailableProfilesResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListCodeAnalysisFindings": { - "name": "ListCodeAnalysisFindings", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListCodeAnalysisFindingsRequest" - }, - "output": { - "shape": "ListCodeAnalysisFindingsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListEvents": { - "name": "ListEvents", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListEventsRequest" - }, - "output": { - "shape": "ListEventsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListFeatureEvaluations": { - "name": "ListFeatureEvaluations", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListFeatureEvaluationsRequest" - }, - "output": { - "shape": "ListFeatureEvaluationsResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListUserMemoryEntries": { - "name": "ListUserMemoryEntries", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListUserMemoryEntriesInput" - }, - "output": { - "shape": "ListUserMemoryEntriesOutput" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ListWorkspaceMetadata": { - "name": "ListWorkspaceMetadata", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ListWorkspaceMetadataRequest" - }, - "output": { - "shape": "ListWorkspaceMetadataResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "ResumeTransformation": { - "name": "ResumeTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "ResumeTransformationRequest" - }, - "output": { - "shape": "ResumeTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "SendTelemetryEvent": { - "name": "SendTelemetryEvent", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "SendTelemetryEventRequest" - }, - "output": { - "shape": "SendTelemetryEventResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "StartCodeAnalysis": { - "name": "StartCodeAnalysis", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartCodeAnalysisRequest" - }, - "output": { - "shape": "StartCodeAnalysisResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "StartCodeFixJob": { - "name": "StartCodeFixJob", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartCodeFixJobRequest" - }, - "output": { - "shape": "StartCodeFixJobResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "StartTaskAssistCodeGeneration": { - "name": "StartTaskAssistCodeGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartTaskAssistCodeGenerationRequest" - }, - "output": { - "shape": "StartTaskAssistCodeGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "ServiceQuotaExceededException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "StartTestGeneration": { - "name": "StartTestGeneration", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartTestGenerationRequest" - }, - "output": { - "shape": "StartTestGenerationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ], - "idempotent": true - }, - "StartTransformation": { - "name": "StartTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StartTransformationRequest" - }, - "output": { - "shape": "StartTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ConflictException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - }, - "StopTransformation": { - "name": "StopTransformation", - "http": { - "method": "POST", - "requestUri": "/" - }, - "input": { - "shape": "StopTransformationRequest" - }, - "output": { - "shape": "StopTransformationResponse" - }, - "errors": [ - { - "shape": "ThrottlingException" - }, - { - "shape": "ResourceNotFoundException" - }, - { - "shape": "InternalServerException" - }, - { - "shape": "ValidationException" - }, - { - "shape": "AccessDeniedException" - } - ] - } - }, - "shapes": { - "AccessDeniedException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - }, - "reason": { - "shape": "AccessDeniedExceptionReason" - } - }, - "exception": true - }, - "AccessDeniedExceptionReason": { - "type": "string", - "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] - }, - "ActiveFunctionalityList": { - "type": "list", - "member": { - "shape": "FunctionalityName" - }, - "max": 10, - "min": 0 - }, - "AdditionalContentEntry": { - "type": "structure", - "required": ["name", "description"], - "members": { - "name": { - "shape": "AdditionalContentEntryNameString" - }, - "description": { - "shape": "AdditionalContentEntryDescriptionString" - }, - "innerContext": { - "shape": "AdditionalContentEntryInnerContextString" - } - } - }, - "AdditionalContentEntryDescriptionString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AdditionalContentEntryInnerContextString": { - "type": "string", - "max": 8192, - "min": 1, - "sensitive": true - }, - "AdditionalContentEntryNameString": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "[a-z]+(?:-[a-z0-9]+)*", - "sensitive": true - }, - "AdditionalContentList": { - "type": "list", - "member": { - "shape": "AdditionalContentEntry" - }, - "max": 20, - "min": 0 - }, - "AppStudioState": { - "type": "structure", - "required": ["namespace", "propertyName", "propertyContext"], - "members": { - "namespace": { - "shape": "AppStudioStateNamespaceString" - }, - "propertyName": { - "shape": "AppStudioStatePropertyNameString" - }, - "propertyValue": { - "shape": "AppStudioStatePropertyValueString" - }, - "propertyContext": { - "shape": "AppStudioStatePropertyContextString" - } - } - }, - "AppStudioStateNamespaceString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AppStudioStatePropertyContextString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AppStudioStatePropertyNameString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "AppStudioStatePropertyValueString": { - "type": "string", - "max": 10240, - "min": 0, - "sensitive": true - }, - "ApplicationProperties": { - "type": "structure", - "required": ["tenantId", "applicationArn", "tenantUrl", "applicationType"], - "members": { - "tenantId": { - "shape": "TenantId" - }, - "applicationArn": { - "shape": "ResourceArn" - }, - "tenantUrl": { - "shape": "Url" - }, - "applicationType": { - "shape": "FunctionalityName" - } - } - }, - "ApplicationPropertiesList": { - "type": "list", - "member": { - "shape": "ApplicationProperties" - } - }, - "ArtifactId": { - "type": "string", - "max": 126, - "min": 1, - "pattern": "[a-zA-Z0-9-_]+" - }, - "ArtifactMap": { - "type": "map", - "key": { - "shape": "ArtifactType" - }, - "value": { - "shape": "UploadId" - }, - "max": 64, - "min": 1 - }, - "ArtifactType": { - "type": "string", - "enum": ["SourceCode", "BuiltJars"] - }, - "AssistantResponseMessage": { - "type": "structure", - "required": ["content"], - "members": { - "messageId": { - "shape": "MessageId" - }, - "content": { - "shape": "AssistantResponseMessageContentString" - }, - "supplementaryWebLinks": { - "shape": "SupplementaryWebLinks" - }, - "references": { - "shape": "References" - }, - "followupPrompt": { - "shape": "FollowupPrompt" - }, - "toolUses": { - "shape": "ToolUses" - } - } - }, - "AssistantResponseMessageContentString": { - "type": "string", - "max": 100000, - "min": 0, - "sensitive": true - }, - "AttributesMap": { - "type": "map", - "key": { - "shape": "AttributesMapKeyString" - }, - "value": { - "shape": "StringList" - } - }, - "AttributesMapKeyString": { - "type": "string", - "max": 128, - "min": 1 - }, - "Base64EncodedPaginationToken": { - "type": "string", - "max": 2048, - "min": 1, - "pattern": "(?:[A-Za-z0-9\\+/]{4})*(?:[A-Za-z0-9\\+/]{2}\\=\\=|[A-Za-z0-9\\+/]{3}\\=)?" - }, - "Boolean": { - "type": "boolean", - "box": true - }, - "ByUserAnalytics": { - "type": "structure", - "required": ["toggle"], - "members": { - "s3Uri": { - "shape": "S3Uri" - }, - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "ChatAddMessageEvent": { - "type": "structure", - "required": ["conversationId", "messageId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "messageId": { - "shape": "MessageId" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "userIntent": { - "shape": "UserIntent" - }, - "hasCodeSnippet": { - "shape": "Boolean" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "activeEditorTotalCharacters": { - "shape": "Integer" - }, - "timeToFirstChunkMilliseconds": { - "shape": "Double" - }, - "timeBetweenChunks": { - "shape": "timeBetweenChunks" - }, - "fullResponselatency": { - "shape": "Double" - }, - "requestLength": { - "shape": "Integer" - }, - "responseLength": { - "shape": "Integer" - }, - "numberOfCodeBlocks": { - "shape": "Integer" - }, - "hasProjectLevelContext": { - "shape": "Boolean" - } - } - }, - "ChatHistory": { - "type": "list", - "member": { - "shape": "ChatMessage" - }, - "max": 250, - "min": 0 - }, - "ChatInteractWithMessageEvent": { - "type": "structure", - "required": ["conversationId", "messageId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "messageId": { - "shape": "MessageId" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "interactionType": { - "shape": "ChatMessageInteractionType" - }, - "interactionTarget": { - "shape": "ChatInteractWithMessageEventInteractionTargetString" - }, - "acceptedCharacterCount": { - "shape": "Integer" - }, - "acceptedLineCount": { - "shape": "Integer" - }, - "acceptedSnippetHasReference": { - "shape": "Boolean" - }, - "hasProjectLevelContext": { - "shape": "Boolean" - }, - "userIntent": { - "shape": "UserIntent" - }, - "addedIdeDiagnostics": { - "shape": "IdeDiagnosticList" - }, - "removedIdeDiagnostics": { - "shape": "IdeDiagnosticList" - } - } - }, - "ChatInteractWithMessageEventInteractionTargetString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "ChatMessage": { - "type": "structure", - "members": { - "userInputMessage": { - "shape": "UserInputMessage" - }, - "assistantResponseMessage": { - "shape": "AssistantResponseMessage" - } - }, - "union": true - }, - "ChatMessageInteractionType": { - "type": "string", - "enum": [ - "INSERT_AT_CURSOR", - "COPY_SNIPPET", - "COPY", - "CLICK_LINK", - "CLICK_BODY_LINK", - "CLICK_FOLLOW_UP", - "HOVER_REFERENCE", - "UPVOTE", - "DOWNVOTE" - ] - }, - "ChatTriggerType": { - "type": "string", - "enum": ["MANUAL", "DIAGNOSTIC", "INLINE_CHAT"] - }, - "ChatUserModificationEvent": { - "type": "structure", - "required": ["conversationId", "messageId", "modificationPercentage"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "messageId": { - "shape": "MessageId" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "modificationPercentage": { - "shape": "Double" - }, - "hasProjectLevelContext": { - "shape": "Boolean" - } - } - }, - "ClientId": { - "type": "string", - "max": 255, - "min": 1 - }, - "CodeAnalysisFindingsSchema": { - "type": "string", - "enum": ["codeanalysis/findings/1.0"] - }, - "CodeAnalysisScope": { - "type": "string", - "enum": ["FILE", "PROJECT"] - }, - "CodeAnalysisStatus": { - "type": "string", - "enum": ["Completed", "Pending", "Failed"] - }, - "CodeAnalysisUploadContext": { - "type": "structure", - "required": ["codeScanName"], - "members": { - "codeScanName": { - "shape": "CodeScanName" - } - } - }, - "CodeCoverageEvent": { - "type": "structure", - "required": ["programmingLanguage", "acceptedCharacterCount", "totalCharacterCount", "timestamp"], - "members": { - "customizationArn": { - "shape": "CustomizationArn" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "acceptedCharacterCount": { - "shape": "PrimitiveInteger" - }, - "totalCharacterCount": { - "shape": "PrimitiveInteger" - }, - "timestamp": { - "shape": "Timestamp" - }, - "unmodifiedAcceptedCharacterCount": { - "shape": "PrimitiveInteger" - }, - "totalNewCodeCharacterCount": { - "shape": "PrimitiveInteger" - }, - "totalNewCodeLineCount": { - "shape": "PrimitiveInteger" - }, - "userWrittenCodeCharacterCount": { - "shape": "CodeCoverageEventUserWrittenCodeCharacterCountInteger" - }, - "userWrittenCodeLineCount": { - "shape": "CodeCoverageEventUserWrittenCodeLineCountInteger" - } - } - }, - "CodeCoverageEventUserWrittenCodeCharacterCountInteger": { - "type": "integer", - "min": 0 - }, - "CodeCoverageEventUserWrittenCodeLineCountInteger": { - "type": "integer", - "min": 0 - }, - "CodeDescription": { - "type": "structure", - "required": ["href"], - "members": { - "href": { - "shape": "CodeDescriptionHrefString" - } - } - }, - "CodeDescriptionHrefString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "CodeFixAcceptanceEvent": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "String" - }, - "ruleId": { - "shape": "String" - }, - "detectorId": { - "shape": "String" - }, - "findingId": { - "shape": "String" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "linesOfCodeAccepted": { - "shape": "Integer" - }, - "charsOfCodeAccepted": { - "shape": "Integer" - } - } - }, - "CodeFixGenerationEvent": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "String" - }, - "ruleId": { - "shape": "String" - }, - "detectorId": { - "shape": "String" - }, - "findingId": { - "shape": "String" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "linesOfCodeGenerated": { - "shape": "Integer" - }, - "charsOfCodeGenerated": { - "shape": "Integer" - } - } - }, - "CodeFixJobStatus": { - "type": "string", - "enum": ["Succeeded", "InProgress", "Failed"] - }, - "CodeFixName": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[a-zA-Z0-9-_$:.]*" - }, - "CodeFixUploadContext": { - "type": "structure", - "required": ["codeFixName"], - "members": { - "codeFixName": { - "shape": "CodeFixName" - } - } - }, - "CodeGenerationId": { - "type": "string", - "max": 128, - "min": 1 - }, - "CodeGenerationStatus": { - "type": "structure", - "required": ["status", "currentStage"], - "members": { - "status": { - "shape": "CodeGenerationWorkflowStatus" - }, - "currentStage": { - "shape": "CodeGenerationWorkflowStage" - } - } - }, - "CodeGenerationStatusDetail": { - "type": "string", - "sensitive": true - }, - "CodeGenerationWorkflowStage": { - "type": "string", - "enum": ["InitialCodeGeneration", "CodeRefinement"] - }, - "CodeGenerationWorkflowStatus": { - "type": "string", - "enum": ["InProgress", "Complete", "Failed"] - }, - "CodeScanEvent": { - "type": "structure", - "required": ["programmingLanguage", "codeScanJobId", "timestamp"], - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "codeScanJobId": { - "shape": "CodeScanJobId" - }, - "timestamp": { - "shape": "Timestamp" - }, - "codeAnalysisScope": { - "shape": "CodeAnalysisScope" - } - } - }, - "CodeScanFailedEvent": { - "type": "structure", - "required": ["programmingLanguage", "codeScanJobId", "timestamp"], - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "codeScanJobId": { - "shape": "CodeScanJobId" - }, - "timestamp": { - "shape": "Timestamp" - }, - "codeAnalysisScope": { - "shape": "CodeAnalysisScope" - } - } - }, - "CodeScanJobId": { - "type": "string", - "max": 128, - "min": 1 - }, - "CodeScanName": { - "type": "string", - "max": 128, - "min": 1 - }, - "CodeScanRemediationsEvent": { - "type": "structure", - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "CodeScanRemediationsEventType": { - "shape": "CodeScanRemediationsEventType" - }, - "timestamp": { - "shape": "Timestamp" - }, - "detectorId": { - "shape": "String" - }, - "findingId": { - "shape": "String" - }, - "ruleId": { - "shape": "String" - }, - "component": { - "shape": "String" - }, - "reason": { - "shape": "String" - }, - "result": { - "shape": "String" - }, - "includesFix": { - "shape": "Boolean" - } - } - }, - "CodeScanRemediationsEventType": { - "type": "string", - "enum": ["CODESCAN_ISSUE_HOVER", "CODESCAN_ISSUE_APPLY_FIX", "CODESCAN_ISSUE_VIEW_DETAILS"] - }, - "CodeScanSucceededEvent": { - "type": "structure", - "required": ["programmingLanguage", "codeScanJobId", "timestamp", "numberOfFindings"], - "members": { - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "codeScanJobId": { - "shape": "CodeScanJobId" - }, - "timestamp": { - "shape": "Timestamp" - }, - "numberOfFindings": { - "shape": "PrimitiveInteger" - }, - "codeAnalysisScope": { - "shape": "CodeAnalysisScope" - } - } - }, - "Completion": { - "type": "structure", - "required": ["content"], - "members": { - "content": { - "shape": "CompletionContentString" - }, - "references": { - "shape": "References" - }, - "mostRelevantMissingImports": { - "shape": "Imports" - } - } - }, - "CompletionContentString": { - "type": "string", - "max": 5120, - "min": 1, - "sensitive": true - }, - "CompletionType": { - "type": "string", - "enum": ["BLOCK", "LINE"] - }, - "Completions": { - "type": "list", - "member": { - "shape": "Completion" - }, - "max": 10, - "min": 0 - }, - "ConflictException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - }, - "reason": { - "shape": "ConflictExceptionReason" - } - }, - "exception": true - }, - "ConflictExceptionReason": { - "type": "string", - "enum": ["CUSTOMER_KMS_KEY_INVALID_KEY_POLICY", "CUSTOMER_KMS_KEY_DISABLED", "MISMATCHED_KMS_KEY"] - }, - "ConsoleState": { - "type": "structure", - "members": { - "region": { - "shape": "String" - }, - "consoleUrl": { - "shape": "SensitiveString" - }, - "serviceId": { - "shape": "String" - }, - "serviceConsolePage": { - "shape": "String" - }, - "serviceSubconsolePage": { - "shape": "String" - }, - "taskName": { - "shape": "SensitiveString" - } - } - }, - "ContentChecksumType": { - "type": "string", - "enum": ["SHA_256"] - }, - "ContextTruncationScheme": { - "type": "string", - "enum": ["ANALYSIS", "GUMBY"] - }, - "ConversationId": { - "type": "string", - "max": 128, - "min": 1 - }, - "ConversationState": { - "type": "structure", - "required": ["currentMessage", "chatTriggerType"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "history": { - "shape": "ChatHistory" - }, - "currentMessage": { - "shape": "ChatMessage" - }, - "chatTriggerType": { - "shape": "ChatTriggerType" - }, - "customizationArn": { - "shape": "ResourceArn" - } - } - }, - "CreateTaskAssistConversationRequest": { - "type": "structure", - "members": { - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "CreateTaskAssistConversationResponse": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - } - } - }, - "CreateUploadUrlRequest": { - "type": "structure", - "members": { - "contentMd5": { - "shape": "CreateUploadUrlRequestContentMd5String" - }, - "contentChecksum": { - "shape": "CreateUploadUrlRequestContentChecksumString" - }, - "contentChecksumType": { - "shape": "ContentChecksumType" - }, - "contentLength": { - "shape": "CreateUploadUrlRequestContentLengthLong" - }, - "artifactType": { - "shape": "ArtifactType" - }, - "uploadIntent": { - "shape": "UploadIntent" - }, - "uploadContext": { - "shape": "UploadContext" - }, - "uploadId": { - "shape": "UploadId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "CreateUploadUrlRequestContentChecksumString": { - "type": "string", - "max": 512, - "min": 1, - "sensitive": true - }, - "CreateUploadUrlRequestContentLengthLong": { - "type": "long", - "box": true, - "min": 1 - }, - "CreateUploadUrlRequestContentMd5String": { - "type": "string", - "max": 128, - "min": 1, - "sensitive": true - }, - "CreateUploadUrlResponse": { - "type": "structure", - "required": ["uploadId", "uploadUrl"], - "members": { - "uploadId": { - "shape": "UploadId" - }, - "uploadUrl": { - "shape": "PreSignedUrl" - }, - "kmsKeyArn": { - "shape": "ResourceArn" - }, - "requestHeaders": { - "shape": "RequestHeaders" - } - } - }, - "CreateUserMemoryEntryInput": { - "type": "structure", - "required": ["memoryEntryString", "origin"], - "members": { - "memoryEntryString": { - "shape": "CreateUserMemoryEntryInputMemoryEntryStringString" - }, - "origin": { - "shape": "Origin" - }, - "profileArn": { - "shape": "CreateUserMemoryEntryInputProfileArnString" - }, - "clientToken": { - "shape": "String", - "idempotencyToken": true - } - } - }, - "CreateUserMemoryEntryInputMemoryEntryStringString": { - "type": "string", - "min": 1, - "sensitive": true - }, - "CreateUserMemoryEntryInputProfileArnString": { - "type": "string", - "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "CreateUserMemoryEntryOutput": { - "type": "structure", - "required": ["memoryEntry"], - "members": { - "memoryEntry": { - "shape": "MemoryEntry" - } - } - }, - "CreateWorkspaceRequest": { - "type": "structure", - "required": ["workspaceRoot"], - "members": { - "workspaceRoot": { - "shape": "CreateWorkspaceRequestWorkspaceRootString" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "CreateWorkspaceRequestWorkspaceRootString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "CreateWorkspaceResponse": { - "type": "structure", - "required": ["workspace"], - "members": { - "workspace": { - "shape": "WorkspaceMetadata" - } - } - }, - "CursorState": { - "type": "structure", - "members": { - "position": { - "shape": "Position" - }, - "range": { - "shape": "Range" - } - }, - "union": true - }, - "Customization": { - "type": "structure", - "required": ["arn"], - "members": { - "arn": { - "shape": "CustomizationArn" - }, - "name": { - "shape": "CustomizationName" - }, - "description": { - "shape": "Description" - } - } - }, - "CustomizationArn": { - "type": "string", - "max": 950, - "min": 0, - "pattern": "arn:[-.a-z0-9]{1,63}:codewhisperer:([-.a-z0-9]{0,63}:){2}([a-zA-Z0-9-_:/]){1,1023}" - }, - "CustomizationName": { - "type": "string", - "max": 100, - "min": 1, - "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" - }, - "Customizations": { - "type": "list", - "member": { - "shape": "Customization" - } - }, - "DashboardAnalytics": { - "type": "structure", - "required": ["toggle"], - "members": { - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "DeleteTaskAssistConversationRequest": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "DeleteTaskAssistConversationResponse": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - } - } - }, - "DeleteUserMemoryEntryInput": { - "type": "structure", - "required": ["id"], - "members": { - "id": { - "shape": "DeleteUserMemoryEntryInputIdString" - }, - "profileArn": { - "shape": "DeleteUserMemoryEntryInputProfileArnString" - } - } - }, - "DeleteUserMemoryEntryInputIdString": { - "type": "string", - "max": 36, - "min": 36, - "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" - }, - "DeleteUserMemoryEntryInputProfileArnString": { - "type": "string", - "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "DeleteUserMemoryEntryOutput": { - "type": "structure", - "members": {} - }, - "DeleteWorkspaceRequest": { - "type": "structure", - "required": ["workspaceId"], - "members": { - "workspaceId": { - "shape": "UUID" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "DeleteWorkspaceResponse": { - "type": "structure", - "members": {} - }, - "Description": { - "type": "string", - "max": 256, - "min": 0, - "pattern": "[\\sa-zA-Z0-9_-]*" - }, - "Diagnostic": { - "type": "structure", - "members": { - "textDocumentDiagnostic": { - "shape": "TextDocumentDiagnostic" - }, - "runtimeDiagnostic": { - "shape": "RuntimeDiagnostic" - } - }, - "union": true - }, - "DiagnosticLocation": { - "type": "structure", - "required": ["uri", "range"], - "members": { - "uri": { - "shape": "DiagnosticLocationUriString" - }, - "range": { - "shape": "Range" - } - } - }, - "DiagnosticLocationUriString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "DiagnosticRelatedInformation": { - "type": "structure", - "required": ["location", "message"], - "members": { - "location": { - "shape": "DiagnosticLocation" - }, - "message": { - "shape": "DiagnosticRelatedInformationMessageString" - } - } - }, - "DiagnosticRelatedInformationList": { - "type": "list", - "member": { - "shape": "DiagnosticRelatedInformation" - }, - "max": 1024, - "min": 0 - }, - "DiagnosticRelatedInformationMessageString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "DiagnosticSeverity": { - "type": "string", - "enum": ["ERROR", "WARNING", "INFORMATION", "HINT"] - }, - "DiagnosticTag": { - "type": "string", - "enum": ["UNNECESSARY", "DEPRECATED"] - }, - "DiagnosticTagList": { - "type": "list", - "member": { - "shape": "DiagnosticTag" - }, - "max": 1024, - "min": 0 - }, - "Dimension": { - "type": "structure", - "members": { - "name": { - "shape": "DimensionNameString" - }, - "value": { - "shape": "DimensionValueString" - } - } - }, - "DimensionList": { - "type": "list", - "member": { - "shape": "Dimension" - }, - "max": 30, - "min": 0 - }, - "DimensionNameString": { - "type": "string", - "max": 255, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "DimensionValueString": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "DocFolderLevel": { - "type": "string", - "enum": ["SUB_FOLDER", "ENTIRE_WORKSPACE"] - }, - "DocGenerationEvent": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "numberOfAddChars": { - "shape": "PrimitiveInteger" - }, - "numberOfAddLines": { - "shape": "PrimitiveInteger" - }, - "numberOfAddFiles": { - "shape": "PrimitiveInteger" - }, - "userDecision": { - "shape": "DocUserDecision" - }, - "interactionType": { - "shape": "DocInteractionType" - }, - "userIdentity": { - "shape": "String" - }, - "numberOfNavigation": { - "shape": "PrimitiveInteger" - }, - "folderLevel": { - "shape": "DocFolderLevel" - } - } - }, - "DocInteractionType": { - "type": "string", - "enum": ["GENERATE_README", "UPDATE_README", "EDIT_README"] - }, - "DocUserDecision": { - "type": "string", - "enum": ["ACCEPT", "REJECT"] - }, - "DocV2AcceptanceEvent": { - "type": "structure", - "required": [ - "conversationId", - "numberOfAddedChars", - "numberOfAddedLines", - "numberOfAddedFiles", - "userDecision", - "interactionType", - "numberOfNavigations", - "folderLevel" - ], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "numberOfAddedChars": { - "shape": "DocV2AcceptanceEventNumberOfAddedCharsInteger" - }, - "numberOfAddedLines": { - "shape": "DocV2AcceptanceEventNumberOfAddedLinesInteger" - }, - "numberOfAddedFiles": { - "shape": "DocV2AcceptanceEventNumberOfAddedFilesInteger" - }, - "userDecision": { - "shape": "DocUserDecision" - }, - "interactionType": { - "shape": "DocInteractionType" - }, - "numberOfNavigations": { - "shape": "DocV2AcceptanceEventNumberOfNavigationsInteger" - }, - "folderLevel": { - "shape": "DocFolderLevel" - } - } - }, - "DocV2AcceptanceEventNumberOfAddedCharsInteger": { - "type": "integer", - "min": 0 - }, - "DocV2AcceptanceEventNumberOfAddedFilesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2AcceptanceEventNumberOfAddedLinesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2AcceptanceEventNumberOfNavigationsInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEvent": { - "type": "structure", - "required": [ - "conversationId", - "numberOfGeneratedChars", - "numberOfGeneratedLines", - "numberOfGeneratedFiles" - ], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "numberOfGeneratedChars": { - "shape": "DocV2GenerationEventNumberOfGeneratedCharsInteger" - }, - "numberOfGeneratedLines": { - "shape": "DocV2GenerationEventNumberOfGeneratedLinesInteger" - }, - "numberOfGeneratedFiles": { - "shape": "DocV2GenerationEventNumberOfGeneratedFilesInteger" - }, - "interactionType": { - "shape": "DocInteractionType" - }, - "numberOfNavigations": { - "shape": "DocV2GenerationEventNumberOfNavigationsInteger" - }, - "folderLevel": { - "shape": "DocFolderLevel" - } - } - }, - "DocV2GenerationEventNumberOfGeneratedCharsInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEventNumberOfGeneratedFilesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEventNumberOfGeneratedLinesInteger": { - "type": "integer", - "min": 0 - }, - "DocV2GenerationEventNumberOfNavigationsInteger": { - "type": "integer", - "min": 0 - }, - "DocumentSymbol": { - "type": "structure", - "required": ["name", "type"], - "members": { - "name": { - "shape": "DocumentSymbolNameString" - }, - "type": { - "shape": "SymbolType" - }, - "source": { - "shape": "DocumentSymbolSourceString" - } - } - }, - "DocumentSymbolNameString": { - "type": "string", - "max": 256, - "min": 1 - }, - "DocumentSymbolSourceString": { - "type": "string", - "max": 256, - "min": 1 - }, - "DocumentSymbols": { - "type": "list", - "member": { - "shape": "DocumentSymbol" - }, - "max": 1000, - "min": 0 - }, - "DocumentationIntentContext": { - "type": "structure", - "required": ["type"], - "members": { - "scope": { - "shape": "DocumentationIntentContextScopeString" - }, - "type": { - "shape": "DocumentationType" - } - } - }, - "DocumentationIntentContextScopeString": { - "type": "string", - "max": 4096, - "min": 1, - "sensitive": true - }, - "DocumentationType": { - "type": "string", - "enum": ["README"] - }, - "Double": { - "type": "double", - "box": true - }, - "EditorState": { - "type": "structure", - "members": { - "document": { - "shape": "TextDocument" - }, - "cursorState": { - "shape": "CursorState" - }, - "relevantDocuments": { - "shape": "RelevantDocumentList" - }, - "useRelevantDocuments": { - "shape": "Boolean" - }, - "workspaceFolders": { - "shape": "WorkspaceFolderList" - } - } - }, - "EnvState": { - "type": "structure", - "members": { - "operatingSystem": { - "shape": "EnvStateOperatingSystemString" - }, - "currentWorkingDirectory": { - "shape": "EnvStateCurrentWorkingDirectoryString" - }, - "environmentVariables": { - "shape": "EnvironmentVariables" - }, - "timezoneOffset": { - "shape": "EnvStateTimezoneOffsetInteger" - } - } - }, - "EnvStateCurrentWorkingDirectoryString": { - "type": "string", - "max": 256, - "min": 1, - "sensitive": true - }, - "EnvStateOperatingSystemString": { - "type": "string", - "max": 32, - "min": 1, - "pattern": "(macos|linux|windows)" - }, - "EnvStateTimezoneOffsetInteger": { - "type": "integer", - "box": true, - "max": 1440, - "min": -1440 - }, - "EnvironmentVariable": { - "type": "structure", - "members": { - "key": { - "shape": "EnvironmentVariableKeyString" - }, - "value": { - "shape": "EnvironmentVariableValueString" - } - } - }, - "EnvironmentVariableKeyString": { - "type": "string", - "max": 256, - "min": 1, - "sensitive": true - }, - "EnvironmentVariableValueString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "EnvironmentVariables": { - "type": "list", - "member": { - "shape": "EnvironmentVariable" - }, - "max": 100, - "min": 0 - }, - "ErrorDetails": { - "type": "string", - "max": 2048, - "min": 0 - }, - "Event": { - "type": "structure", - "required": ["eventId", "generationId", "eventTimestamp", "eventType", "eventBlob"], - "members": { - "eventId": { - "shape": "UUID" - }, - "generationId": { - "shape": "UUID" - }, - "eventTimestamp": { - "shape": "SyntheticTimestamp_date_time" - }, - "eventType": { - "shape": "EventType" - }, - "eventBlob": { - "shape": "EventBlob" - } - } - }, - "EventBlob": { - "type": "blob", - "max": 400000, - "min": 1, - "sensitive": true - }, - "EventList": { - "type": "list", - "member": { - "shape": "Event" - }, - "max": 10, - "min": 1 - }, - "EventType": { - "type": "string", - "max": 100, - "min": 1 - }, - "ExternalIdentityDetails": { - "type": "structure", - "members": { - "issuerUrl": { - "shape": "IssuerUrl" - }, - "clientId": { - "shape": "ClientId" - }, - "scimEndpoint": { - "shape": "String" - } - } - }, - "FeatureDevCodeAcceptanceEvent": { - "type": "structure", - "required": ["conversationId", "linesOfCodeAccepted", "charactersOfCodeAccepted"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "linesOfCodeAccepted": { - "shape": "FeatureDevCodeAcceptanceEventLinesOfCodeAcceptedInteger" - }, - "charactersOfCodeAccepted": { - "shape": "FeatureDevCodeAcceptanceEventCharactersOfCodeAcceptedInteger" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "FeatureDevCodeAcceptanceEventCharactersOfCodeAcceptedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevCodeAcceptanceEventLinesOfCodeAcceptedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevCodeGenerationEvent": { - "type": "structure", - "required": ["conversationId", "linesOfCodeGenerated", "charactersOfCodeGenerated"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "linesOfCodeGenerated": { - "shape": "FeatureDevCodeGenerationEventLinesOfCodeGeneratedInteger" - }, - "charactersOfCodeGenerated": { - "shape": "FeatureDevCodeGenerationEventCharactersOfCodeGeneratedInteger" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "FeatureDevCodeGenerationEventCharactersOfCodeGeneratedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevCodeGenerationEventLinesOfCodeGeneratedInteger": { - "type": "integer", - "min": 0 - }, - "FeatureDevEvent": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - } - } - }, - "FeatureEvaluation": { - "type": "structure", - "required": ["feature", "variation", "value"], - "members": { - "feature": { - "shape": "FeatureName" - }, - "variation": { - "shape": "FeatureVariation" - }, - "value": { - "shape": "FeatureValue" - } - } - }, - "FeatureEvaluationsList": { - "type": "list", - "member": { - "shape": "FeatureEvaluation" - }, - "max": 50, - "min": 0 - }, - "FeatureName": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "FeatureValue": { - "type": "structure", - "members": { - "boolValue": { - "shape": "Boolean" - }, - "doubleValue": { - "shape": "Double" - }, - "longValue": { - "shape": "Long" - }, - "stringValue": { - "shape": "FeatureValueStringType" - } - }, - "union": true - }, - "FeatureValueStringType": { - "type": "string", - "max": 512, - "min": 0 - }, - "FeatureVariation": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "FileContext": { - "type": "structure", - "required": ["leftFileContent", "rightFileContent", "filename", "programmingLanguage"], - "members": { - "leftFileContent": { - "shape": "FileContextLeftFileContentString" - }, - "rightFileContent": { - "shape": "FileContextRightFileContentString" - }, - "filename": { - "shape": "FileContextFilenameString" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "FileContextFilenameString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "FileContextLeftFileContentString": { - "type": "string", - "max": 10240, - "min": 0, - "sensitive": true - }, - "FileContextRightFileContentString": { - "type": "string", - "max": 10240, - "min": 0, - "sensitive": true - }, - "FollowupPrompt": { - "type": "structure", - "required": ["content"], - "members": { - "content": { - "shape": "FollowupPromptContentString" - }, - "userIntent": { - "shape": "UserIntent" - } - } - }, - "FollowupPromptContentString": { - "type": "string", - "max": 4096, - "min": 0, - "sensitive": true - }, - "FunctionalityName": { - "type": "string", - "enum": [ - "COMPLETIONS", - "ANALYSIS", - "CONVERSATIONS", - "TASK_ASSIST", - "TRANSFORMATIONS", - "CHAT_CUSTOMIZATION", - "TRANSFORMATIONS_WEBAPP", - "FEATURE_DEVELOPMENT" - ], - "max": 64, - "min": 1 - }, - "GenerateCompletionsRequest": { - "type": "structure", - "required": ["fileContext"], - "members": { - "fileContext": { - "shape": "FileContext" - }, - "maxResults": { - "shape": "GenerateCompletionsRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "GenerateCompletionsRequestNextTokenString" - }, - "referenceTrackerConfiguration": { - "shape": "ReferenceTrackerConfiguration" - }, - "supplementalContexts": { - "shape": "SupplementalContextList" - }, - "customizationArn": { - "shape": "CustomizationArn" - }, - "optOutPreference": { - "shape": "OptOutPreference" - }, - "userContext": { - "shape": "UserContext" - }, - "profileArn": { - "shape": "ProfileArn" - }, - "workspaceId": { - "shape": "UUID" - } - } - }, - "GenerateCompletionsRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 10, - "min": 1 - }, - "GenerateCompletionsRequestNextTokenString": { - "type": "string", - "max": 2048, - "min": 0, - "pattern": "(?:[A-Za-z0-9\\+/]{4})*(?:[A-Za-z0-9\\+/]{2}\\=\\=|[A-Za-z0-9\\+/]{3}\\=)?", - "sensitive": true - }, - "GenerateCompletionsResponse": { - "type": "structure", - "members": { - "completions": { - "shape": "Completions" - }, - "nextToken": { - "shape": "SensitiveString" - } - } - }, - "GetCodeAnalysisRequest": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "GetCodeAnalysisRequestJobIdString" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetCodeAnalysisRequestJobIdString": { - "type": "string", - "max": 256, - "min": 1 - }, - "GetCodeAnalysisResponse": { - "type": "structure", - "required": ["status"], - "members": { - "status": { - "shape": "CodeAnalysisStatus" - }, - "errorMessage": { - "shape": "SensitiveString" - } - } - }, - "GetCodeFixJobRequest": { - "type": "structure", - "required": ["jobId"], - "members": { - "jobId": { - "shape": "GetCodeFixJobRequestJobIdString" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetCodeFixJobRequestJobIdString": { - "type": "string", - "max": 256, - "min": 1, - "pattern": ".*[A-Za-z0-9-:]+.*" - }, - "GetCodeFixJobResponse": { - "type": "structure", - "members": { - "jobStatus": { - "shape": "CodeFixJobStatus" - }, - "suggestedFix": { - "shape": "SuggestedFix" - } - } - }, - "GetTaskAssistCodeGenerationRequest": { - "type": "structure", - "required": ["conversationId", "codeGenerationId"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "codeGenerationId": { - "shape": "CodeGenerationId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTaskAssistCodeGenerationResponse": { - "type": "structure", - "required": ["conversationId", "codeGenerationStatus"], - "members": { - "conversationId": { - "shape": "ConversationId" - }, - "codeGenerationStatus": { - "shape": "CodeGenerationStatus" - }, - "codeGenerationStatusDetail": { - "shape": "CodeGenerationStatusDetail" - }, - "codeGenerationRemainingIterationCount": { - "shape": "Integer" - }, - "codeGenerationTotalIterationCount": { - "shape": "Integer" - } - } - }, - "GetTestGenerationRequest": { - "type": "structure", - "required": ["testGenerationJobGroupName", "testGenerationJobId"], - "members": { - "testGenerationJobGroupName": { - "shape": "TestGenerationJobGroupName" - }, - "testGenerationJobId": { - "shape": "UUID" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTestGenerationResponse": { - "type": "structure", - "members": { - "testGenerationJob": { - "shape": "TestGenerationJob" - } - } - }, - "GetTransformationPlanRequest": { - "type": "structure", - "required": ["transformationJobId"], - "members": { - "transformationJobId": { - "shape": "TransformationJobId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTransformationPlanResponse": { - "type": "structure", - "required": ["transformationPlan"], - "members": { - "transformationPlan": { - "shape": "TransformationPlan" - } - } - }, - "GetTransformationRequest": { - "type": "structure", - "required": ["transformationJobId"], - "members": { - "transformationJobId": { - "shape": "TransformationJobId" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "GetTransformationResponse": { - "type": "structure", - "required": ["transformationJob"], - "members": { - "transformationJob": { - "shape": "TransformationJob" - } - } - }, - "GitState": { - "type": "structure", - "members": { - "status": { - "shape": "GitStateStatusString" - } - } - }, - "GitStateStatusString": { - "type": "string", - "max": 4096, - "min": 0, - "sensitive": true - }, - "IdeCategory": { - "type": "string", - "enum": ["JETBRAINS", "VSCODE", "CLI", "JUPYTER_MD", "JUPYTER_SM", "ECLIPSE", "VISUAL_STUDIO"], - "max": 64, - "min": 1 - }, - "IdeDiagnostic": { - "type": "structure", - "required": ["ideDiagnosticType"], - "members": { - "range": { - "shape": "Range" - }, - "source": { - "shape": "IdeDiagnosticSourceString" - }, - "severity": { - "shape": "DiagnosticSeverity" - }, - "ideDiagnosticType": { - "shape": "IdeDiagnosticType" - } - } - }, - "IdeDiagnosticList": { - "type": "list", - "member": { - "shape": "IdeDiagnostic" - }, - "max": 1024, - "min": 0 - }, - "IdeDiagnosticSourceString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "IdeDiagnosticType": { - "type": "string", - "enum": ["SYNTAX_ERROR", "TYPE_ERROR", "REFERENCE_ERROR", "BEST_PRACTICE", "SECURITY", "OTHER"] - }, - "IdempotencyToken": { - "type": "string", - "max": 256, - "min": 1 - }, - "IdentityDetails": { - "type": "structure", - "members": { - "ssoIdentityDetails": { - "shape": "SSOIdentityDetails" - }, - "externalIdentityDetails": { - "shape": "ExternalIdentityDetails" - } - }, - "union": true - }, - "ImageBlock": { - "type": "structure", - "required": ["format", "source"], - "members": { - "format": { - "shape": "ImageFormat" - }, - "source": { - "shape": "ImageSource" - } - } - }, - "ImageBlocks": { - "type": "list", - "member": { - "shape": "ImageBlock" - }, - "max": 10, - "min": 0 - }, - "ImageFormat": { - "type": "string", - "enum": ["png", "jpeg", "gif", "webp"] - }, - "ImageSource": { - "type": "structure", - "members": { - "bytes": { - "shape": "ImageSourceBytesBlob" - } - }, - "sensitive": true, - "union": true - }, - "ImageSourceBytesBlob": { - "type": "blob", - "max": 1500000, - "min": 1 - }, - "Import": { - "type": "structure", - "members": { - "statement": { - "shape": "ImportStatementString" - } - } - }, - "ImportStatementString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "Imports": { - "type": "list", - "member": { - "shape": "Import" - }, - "max": 10, - "min": 0 - }, - "InlineChatEvent": { - "type": "structure", - "required": ["requestId", "timestamp"], - "members": { - "requestId": { - "shape": "UUID" - }, - "timestamp": { - "shape": "Timestamp" - }, - "inputLength": { - "shape": "PrimitiveInteger" - }, - "numSelectedLines": { - "shape": "PrimitiveInteger" - }, - "numSuggestionAddChars": { - "shape": "PrimitiveInteger" - }, - "numSuggestionAddLines": { - "shape": "PrimitiveInteger" - }, - "numSuggestionDelChars": { - "shape": "PrimitiveInteger" - }, - "numSuggestionDelLines": { - "shape": "PrimitiveInteger" - }, - "codeIntent": { - "shape": "Boolean" - }, - "userDecision": { - "shape": "InlineChatUserDecision" - }, - "responseStartLatency": { - "shape": "Double" - }, - "responseEndLatency": { - "shape": "Double" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - } - } - }, - "InlineChatUserDecision": { - "type": "string", - "enum": ["ACCEPT", "REJECT", "DISMISS"] - }, - "Integer": { - "type": "integer", - "box": true - }, - "Intent": { - "type": "string", - "enum": ["DEV", "DOC"] - }, - "IntentContext": { - "type": "structure", - "members": { - "documentation": { - "shape": "DocumentationIntentContext" - } - }, - "union": true - }, - "InternalServerException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - } - }, - "exception": true, - "fault": true, - "retryable": { - "throttling": false - } - }, - "IssuerUrl": { - "type": "string", - "max": 255, - "min": 1 - }, - "LineRangeList": { - "type": "list", - "member": { - "shape": "Range" - } - }, - "ListAvailableCustomizationsRequest": { - "type": "structure", - "members": { - "maxResults": { - "shape": "ListAvailableCustomizationsRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListAvailableCustomizationsRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 100, - "min": 1 - }, - "ListAvailableCustomizationsResponse": { - "type": "structure", - "required": ["customizations"], - "members": { - "customizations": { - "shape": "Customizations" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - } - } - }, - "ListAvailableProfilesRequest": { - "type": "structure", - "members": { - "maxResults": { - "shape": "ListAvailableProfilesRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - } - } - }, - "ListAvailableProfilesRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 10, - "min": 1 - }, - "ListAvailableProfilesResponse": { - "type": "structure", - "required": ["profiles"], - "members": { - "profiles": { - "shape": "ProfileList" - }, - "nextToken": { - "shape": "Base64EncodedPaginationToken" - } - } - }, - "ListCodeAnalysisFindingsRequest": { - "type": "structure", - "required": ["jobId", "codeAnalysisFindingsSchema"], - "members": { - "jobId": { - "shape": "ListCodeAnalysisFindingsRequestJobIdString" - }, - "nextToken": { - "shape": "PaginationToken" - }, - "codeAnalysisFindingsSchema": { - "shape": "CodeAnalysisFindingsSchema" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListCodeAnalysisFindingsRequestJobIdString": { - "type": "string", - "max": 256, - "min": 1 - }, - "ListCodeAnalysisFindingsResponse": { - "type": "structure", - "required": ["codeAnalysisFindings"], - "members": { - "nextToken": { - "shape": "PaginationToken" - }, - "codeAnalysisFindings": { - "shape": "SensitiveString" - } - } - }, - "ListEventsRequest": { - "type": "structure", - "required": ["conversationId"], - "members": { - "conversationId": { - "shape": "UUID" - }, - "maxResults": { - "shape": "ListEventsRequestMaxResultsInteger" - }, - "nextToken": { - "shape": "NextToken" - } - } - }, - "ListEventsRequestMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 50, - "min": 1 - }, - "ListEventsResponse": { - "type": "structure", - "required": ["conversationId", "events"], - "members": { - "conversationId": { - "shape": "UUID" - }, - "events": { - "shape": "EventList" - }, - "nextToken": { - "shape": "NextToken" - } - } - }, - "ListFeatureEvaluationsRequest": { - "type": "structure", - "required": ["userContext"], - "members": { - "userContext": { - "shape": "UserContext" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListFeatureEvaluationsResponse": { - "type": "structure", - "required": ["featureEvaluations"], - "members": { - "featureEvaluations": { - "shape": "FeatureEvaluationsList" - } - } - }, - "ListUserMemoryEntriesInput": { - "type": "structure", - "members": { - "maxResults": { - "shape": "ListUserMemoryEntriesInputMaxResultsInteger" - }, - "profileArn": { - "shape": "ListUserMemoryEntriesInputProfileArnString" - }, - "nextToken": { - "shape": "ListUserMemoryEntriesInputNextTokenString" - } - } - }, - "ListUserMemoryEntriesInputMaxResultsInteger": { - "type": "integer", - "box": true, - "max": 100, - "min": 1 - }, - "ListUserMemoryEntriesInputNextTokenString": { - "type": "string", - "min": 1 - }, - "ListUserMemoryEntriesInputProfileArnString": { - "type": "string", - "min": 1, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "ListUserMemoryEntriesOutput": { - "type": "structure", - "required": ["memoryEntries"], - "members": { - "memoryEntries": { - "shape": "MemoryEntryList" - }, - "nextToken": { - "shape": "ListUserMemoryEntriesOutputNextTokenString" - } - } - }, - "ListUserMemoryEntriesOutputNextTokenString": { - "type": "string", - "min": 1 - }, - "ListWorkspaceMetadataRequest": { - "type": "structure", - "members": { - "workspaceRoot": { - "shape": "ListWorkspaceMetadataRequestWorkspaceRootString" - }, - "nextToken": { - "shape": "String" - }, - "maxResults": { - "shape": "Integer" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ListWorkspaceMetadataRequestWorkspaceRootString": { - "type": "string", - "max": 1024, - "min": 1, - "sensitive": true - }, - "ListWorkspaceMetadataResponse": { - "type": "structure", - "required": ["workspaces"], - "members": { - "workspaces": { - "shape": "WorkspaceList" - }, - "nextToken": { - "shape": "String" - } - } - }, - "Long": { - "type": "long", - "box": true - }, - "MemoryEntry": { - "type": "structure", - "required": ["id", "memoryEntryString", "metadata"], - "members": { - "id": { - "shape": "MemoryEntryIdString" - }, - "memoryEntryString": { - "shape": "MemoryEntryMemoryEntryStringString" - }, - "metadata": { - "shape": "MemoryEntryMetadata" - } - } - }, - "MemoryEntryIdString": { - "type": "string", - "max": 36, - "min": 36, - "pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" - }, - "MemoryEntryList": { - "type": "list", - "member": { - "shape": "MemoryEntry" - } - }, - "MemoryEntryMemoryEntryStringString": { - "type": "string", - "max": 500, - "min": 1, - "sensitive": true - }, - "MemoryEntryMetadata": { - "type": "structure", - "required": ["origin", "createdAt", "updatedAt"], - "members": { - "origin": { - "shape": "Origin" - }, - "attributes": { - "shape": "AttributesMap" - }, - "createdAt": { - "shape": "Timestamp" - }, - "updatedAt": { - "shape": "Timestamp" - } - } - }, - "MessageId": { - "type": "string", - "max": 128, - "min": 0 - }, - "MetricData": { - "type": "structure", - "required": ["metricName", "metricValue", "timestamp", "product"], - "members": { - "metricName": { - "shape": "MetricDataMetricNameString" - }, - "metricValue": { - "shape": "Double" - }, - "timestamp": { - "shape": "Timestamp" - }, - "product": { - "shape": "MetricDataProductString" - }, - "dimensions": { - "shape": "DimensionList" - } - } - }, - "MetricDataMetricNameString": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "MetricDataProductString": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "[-a-zA-Z0-9._]*" - }, - "NextToken": { - "type": "string", - "max": 1000, - "min": 0 - }, - "Notifications": { - "type": "list", - "member": { - "shape": "NotificationsFeature" - }, - "max": 10, - "min": 0 - }, - "NotificationsFeature": { - "type": "structure", - "required": ["feature", "toggle"], - "members": { - "feature": { - "shape": "FeatureName" - }, - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "OperatingSystem": { - "type": "string", - "enum": ["MAC", "WINDOWS", "LINUX"], - "max": 64, - "min": 1 - }, - "OptInFeatureToggle": { - "type": "string", - "enum": ["ON", "OFF"] - }, - "OptInFeatures": { - "type": "structure", - "members": { - "promptLogging": { - "shape": "PromptLogging" - }, - "byUserAnalytics": { - "shape": "ByUserAnalytics" - }, - "dashboardAnalytics": { - "shape": "DashboardAnalytics" - }, - "notifications": { - "shape": "Notifications" - }, - "workspaceContext": { - "shape": "WorkspaceContext" - } - } - }, - "OptOutPreference": { - "type": "string", - "enum": ["OPTIN", "OPTOUT"] - }, - "Origin": { - "type": "string", - "enum": [ - "CHATBOT", - "CONSOLE", - "DOCUMENTATION", - "MARKETING", - "MOBILE", - "SERVICE_INTERNAL", - "UNIFIED_SEARCH", - "UNKNOWN", - "MD", - "IDE", - "SAGE_MAKER", - "CLI", - "AI_EDITOR", - "OPENSEARCH_DASHBOARD", - "GITLAB" - ] - }, - "PackageInfo": { - "type": "structure", - "members": { - "executionCommand": { - "shape": "SensitiveString" - }, - "buildCommand": { - "shape": "SensitiveString" - }, - "buildOrder": { - "shape": "PackageInfoBuildOrderInteger" - }, - "testFramework": { - "shape": "String" - }, - "packageSummary": { - "shape": "PackageInfoPackageSummaryString" - }, - "packagePlan": { - "shape": "PackageInfoPackagePlanString" - }, - "targetFileInfoList": { - "shape": "TargetFileInfoList" - } - } - }, - "PackageInfoBuildOrderInteger": { - "type": "integer", - "box": true, - "min": 0 - }, - "PackageInfoList": { - "type": "list", - "member": { - "shape": "PackageInfo" - } - }, - "PackageInfoPackagePlanString": { - "type": "string", - "max": 30720, - "min": 0, - "sensitive": true - }, - "PackageInfoPackageSummaryString": { - "type": "string", - "max": 30720, - "min": 0, - "sensitive": true - }, - "PaginationToken": { - "type": "string", - "max": 2048, - "min": 1, - "pattern": "\\S+" - }, - "Position": { - "type": "structure", - "required": ["line", "character"], - "members": { - "line": { - "shape": "Integer" - }, - "character": { - "shape": "Integer" - } - } - }, - "PreSignedUrl": { - "type": "string", - "max": 2048, - "min": 1, - "sensitive": true - }, - "PrimitiveInteger": { - "type": "integer" - }, - "Profile": { - "type": "structure", - "required": ["arn", "profileName"], - "members": { - "arn": { - "shape": "ProfileArn" - }, - "identityDetails": { - "shape": "IdentityDetails" - }, - "profileName": { - "shape": "ProfileName" - }, - "description": { - "shape": "ProfileDescription" - }, - "referenceTrackerConfiguration": { - "shape": "ReferenceTrackerConfiguration" - }, - "kmsKeyArn": { - "shape": "ResourceArn" - }, - "activeFunctionalities": { - "shape": "ActiveFunctionalityList" - }, - "status": { - "shape": "ProfileStatus" - }, - "errorDetails": { - "shape": "ErrorDetails" - }, - "resourcePolicy": { - "shape": "ResourcePolicy" - }, - "profileType": { - "shape": "ProfileType" - }, - "optInFeatures": { - "shape": "OptInFeatures" - }, - "permissionUpdateRequired": { - "shape": "Boolean" - }, - "applicationProperties": { - "shape": "ApplicationPropertiesList" - } - } - }, - "ProfileArn": { - "type": "string", - "max": 950, - "min": 0, - "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" - }, - "ProfileDescription": { - "type": "string", - "max": 256, - "min": 1, - "pattern": "[\\sa-zA-Z0-9_-]*" - }, - "ProfileList": { - "type": "list", - "member": { - "shape": "Profile" - } - }, - "ProfileName": { - "type": "string", - "max": 100, - "min": 1, - "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" - }, - "ProfileStatus": { - "type": "string", - "enum": ["ACTIVE", "CREATING", "CREATE_FAILED", "UPDATING", "UPDATE_FAILED", "DELETING", "DELETE_FAILED"] - }, - "ProfileType": { - "type": "string", - "enum": ["Q_DEVELOPER", "CODEWHISPERER"] - }, - "ProgrammingLanguage": { - "type": "structure", - "required": ["languageName"], - "members": { - "languageName": { - "shape": "ProgrammingLanguageLanguageNameString" - } - } - }, - "ProgrammingLanguageLanguageNameString": { - "type": "string", - "max": 128, - "min": 1, - "pattern": "(python|javascript|java|csharp|typescript|c|cpp|go|kotlin|php|ruby|rust|scala|shell|sql|json|yaml|vue|tf|tsx|jsx|plaintext|systemverilog|dart|lua|swift|powershell|r)" - }, - "ProgressUpdates": { - "type": "list", - "member": { - "shape": "TransformationProgressUpdate" - } - }, - "PromptLogging": { - "type": "structure", - "required": ["s3Uri", "toggle"], - "members": { - "s3Uri": { - "shape": "S3Uri" - }, - "toggle": { - "shape": "OptInFeatureToggle" - } - } - }, - "Range": { - "type": "structure", - "required": ["start", "end"], - "members": { - "start": { - "shape": "Position" - }, - "end": { - "shape": "Position" - } - } - }, - "RecommendationsWithReferencesPreference": { - "type": "string", - "enum": ["BLOCK", "ALLOW"] - }, - "Reference": { - "type": "structure", - "members": { - "licenseName": { - "shape": "ReferenceLicenseNameString" - }, - "repository": { - "shape": "ReferenceRepositoryString" - }, - "url": { - "shape": "ReferenceUrlString" - }, - "recommendationContentSpan": { - "shape": "Span" - } - } - }, - "ReferenceLicenseNameString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "ReferenceRepositoryString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "ReferenceTrackerConfiguration": { - "type": "structure", - "required": ["recommendationsWithReferences"], - "members": { - "recommendationsWithReferences": { - "shape": "RecommendationsWithReferencesPreference" - } - } - }, - "ReferenceUrlString": { - "type": "string", - "max": 1024, - "min": 1 - }, - "References": { - "type": "list", - "member": { - "shape": "Reference" - }, - "max": 10, - "min": 0 - }, - "RelevantDocumentList": { - "type": "list", - "member": { - "shape": "RelevantTextDocument" - }, - "max": 30, - "min": 0 - }, - "RelevantTextDocument": { - "type": "structure", - "required": ["relativeFilePath"], - "members": { - "relativeFilePath": { - "shape": "RelevantTextDocumentRelativeFilePathString" - }, - "programmingLanguage": { - "shape": "ProgrammingLanguage" - }, - "text": { - "shape": "RelevantTextDocumentTextString" - }, - "documentSymbols": { - "shape": "DocumentSymbols" - } - } - }, - "RelevantTextDocumentRelativeFilePathString": { - "type": "string", - "max": 4096, - "min": 1, - "sensitive": true - }, - "RelevantTextDocumentTextString": { - "type": "string", - "max": 40960, - "min": 0, - "sensitive": true - }, - "RequestHeaderKey": { - "type": "string", - "max": 64, - "min": 1 - }, - "RequestHeaderValue": { - "type": "string", - "max": 256, - "min": 1 - }, - "RequestHeaders": { - "type": "map", - "key": { - "shape": "RequestHeaderKey" - }, - "value": { - "shape": "RequestHeaderValue" - }, - "max": 16, - "min": 1, - "sensitive": true - }, - "ResourceArn": { - "type": "string", - "max": 1224, - "min": 0, - "pattern": "arn:([-.a-z0-9]{1,63}:){2}([-.a-z0-9]{0,63}:){2}([a-zA-Z0-9-_:/]){1,1023}" - }, - "ResourceNotFoundException": { - "type": "structure", - "required": ["message"], - "members": { - "message": { - "shape": "String" - } - }, - "exception": true - }, - "ResourcePolicy": { - "type": "structure", - "required": ["effect"], - "members": { - "effect": { - "shape": "ResourcePolicyEffect" - } - } - }, - "ResourcePolicyEffect": { - "type": "string", - "enum": ["ALLOW", "DENY"] - }, - "ResumeTransformationRequest": { - "type": "structure", - "required": ["transformationJobId"], - "members": { - "transformationJobId": { - "shape": "TransformationJobId" - }, - "userActionStatus": { - "shape": "TransformationUserActionStatus" - }, - "profileArn": { - "shape": "ProfileArn" - } - } - }, - "ResumeTransformationResponse": { - "type": "structure", - "required": ["transformationStatus"], - "members": { - "transformationStatus": { - "shape": "TransformationStatus" - } - } - }, - "RuntimeDiagnostic": { - "type": "structure", - "required": ["source", "severity", "message"], - "members": { - "source": { - "shape": "RuntimeDiagnosticSourceString" - }, - "severity": { - "shape": "DiagnosticSeverity" - }, - "message": { - "shape": "RuntimeDiagnosticMessageString" - } - } - }, - "RuntimeDiagnosticMessageString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "RuntimeDiagnosticSourceString": { - "type": "string", - "max": 1024, - "min": 0, - "sensitive": true - }, - "S3Uri": { - "type": "string", - "max": 1024, - "min": 1, - "pattern": "s3://((?!xn--)[a-z0-9](?![^/]*[.]{2})[a-z0-9-.]{1,61}[a-z0-9](?): Promise { - const bearerToken = await AuthUtil.instance.getBearerToken() - const cwsprConfig = getCodewhispererConfig() - return (await globals.sdkClientBuilder.createAwsService( - Service, - { - apiConfig: apiConfig, - region: cwsprConfig.region, - endpoint: cwsprConfig.endpoint, - token: new Token({ token: bearerToken }), - httpOptions: { - connectTimeout: 10000, // 10 seconds, 3 times P99 API latency - }, - ...options, - } as ServiceOptions, - undefined - )) as FeatureDevProxyClient -} - -export class FeatureDevClient implements FeatureClient { - public async getClient(options?: Partial) { - // Should not be stored for the whole session. - // Client has to be reinitialized for each request so we always have a fresh bearerToken - return await createFeatureDevProxyClient(options) - } - - public async createConversation() { - try { - const client = await this.getClient(writeAPIRetryOptions) - getLogger().debug(`Executing createTaskAssistConversation with {}`) - const { conversationId, $response } = await client - .createTaskAssistConversation({ - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - }) - .promise() - getLogger().debug(`${featureName}: Created conversation: %O`, { - conversationId, - requestId: $response.requestId, - }) - return conversationId - } catch (e) { - if (isAwsError(e)) { - getLogger().error( - `${featureName}: failed to start conversation: ${e.message} RequestId: ${e.requestId}` - ) - // BE service will throw ServiceQuota if conversation limit is reached. API Front-end will throw Throttling with this message if conversation limit is reached - if ( - e.code === 'ServiceQuotaExceededException' || - (e.code === 'ThrottlingException' && e.message.includes('reached for this month.')) - ) { - throw new MonthlyConversationLimitError(e.message) - } - throw ApiError.of(e.message, 'CreateConversation', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'CreateConversation') - } - } - - public async createUploadUrl( - conversationId: string, - contentChecksumSha256: string, - contentLength: number, - uploadId: string - ) { - try { - const client = await this.getClient(writeAPIRetryOptions) - const params: CreateUploadUrlRequest = { - uploadContext: { - taskAssistPlanningUploadContext: { - conversationId, - }, - }, - uploadId, - contentChecksum: contentChecksumSha256, - contentChecksumType: 'SHA_256', - artifactType: 'SourceCode', - uploadIntent: 'TASK_ASSIST_PLANNING', - contentLength, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - getLogger().debug(`Executing createUploadUrl with %O`, omit(params, 'contentChecksum')) - const response = await client.createUploadUrl(params).promise() - getLogger().debug(`${featureName}: Created upload url: %O`, { - uploadId: uploadId, - requestId: response.$response.requestId, - }) - return response - } catch (e) { - if (isAwsError(e)) { - getLogger().error( - `${featureName}: failed to generate presigned url: ${e.message} RequestId: ${e.requestId}` - ) - if (e.code === 'ValidationException' && e.message.includes('Invalid contentLength')) { - throw new ContentLengthError() - } - throw ApiError.of(e.message, 'CreateUploadUrl', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'CreateUploadUrl') - } - } - - public async startCodeGeneration( - conversationId: string, - uploadId: string, - message: string, - intent: FeatureDevProxyClient.Intent, - codeGenerationId: string, - currentCodeGenerationId?: string, - intentContext?: FeatureDevProxyClient.IntentContext - ) { - try { - const client = await this.getClient(writeAPIRetryOptions) - const params: StartTaskAssistCodeGenerationRequest = { - codeGenerationId, - conversationState: { - conversationId, - currentMessage: { - userInputMessage: { content: message }, - }, - chatTriggerType: 'MANUAL', - }, - workspaceState: { - uploadId, - programmingLanguage: { languageName: 'javascript' }, - }, - intent, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - if (currentCodeGenerationId) { - params.currentCodeGenerationId = currentCodeGenerationId - } - if (intentContext) { - params.intentContext = intentContext - } - getLogger().debug(`Executing startTaskAssistCodeGeneration with %O`, params) - const response = await client.startTaskAssistCodeGeneration(params).promise() - - return response - } catch (e) { - getLogger().error( - `${featureName}: failed to start code generation: ${(e as Error).message} RequestId: ${ - (e as any).requestId - }` - ) - if (isAwsError(e)) { - // API Front-end will throw Throttling if conversation limit is reached. API Front-end monitors StartCodeGeneration for throttling - if (e.code === 'ThrottlingException' && e.message.includes(startTaskAssistLimitReachedMessage)) { - throw new MonthlyConversationLimitError(e.message) - } - // BE service will throw ServiceQuota if code generation iteration limit is reached - else if ( - e.code === 'ServiceQuotaExceededException' || - (e.code === 'ThrottlingException' && - e.message.includes('limit for number of iterations on a code generation')) - ) { - throw new CodeIterationLimitError() - } - throw ApiError.of(e.message, 'StartTaskAssistCodeGeneration', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'StartTaskAssistCodeGeneration') - } - } - - public async getCodeGeneration(conversationId: string, codeGenerationId: string) { - try { - const client = await this.getClient() - const params: GetTaskAssistCodeGenerationRequest = { - codeGenerationId, - conversationId, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - getLogger().debug(`Executing getTaskAssistCodeGeneration with %O`, params) - const response = await client.getTaskAssistCodeGeneration(params).promise() - - return response - } catch (e) { - getLogger().error( - `${featureName}: failed to start get code generation results: ${(e as Error).message} RequestId: ${ - (e as any).requestId - }` - ) - - if (isAwsError(e)) { - throw ApiError.of(e.message, 'GetTaskAssistCodeGeneration', e.code, e.statusCode ?? 500) - } - - throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'GetTaskAssistCodeGeneration') - } - } - - public async exportResultArchive(conversationId: string) { - const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile - try { - const streamingClient = await createCodeWhispererChatStreamingClient() - const params = { - exportId: conversationId, - exportIntent: 'TASK_ASSIST', - profileArn: profile?.arn, - } satisfies ExportResultArchiveCommandInput - getLogger().debug(`Executing exportResultArchive with %O`, params) - const archiveResponse = await streamingClient.exportResultArchive(params) - const buffer: number[] = [] - if (archiveResponse.body === undefined) { - throw new ApiServiceError( - 'Empty response from CodeWhisperer Streaming service.', - 'ExportResultArchive', - 'EmptyResponse', - 500 - ) - } - for await (const chunk of archiveResponse.body) { - if (chunk.internalServerException !== undefined) { - throw chunk.internalServerException - } - buffer.push(...(chunk.binaryPayloadEvent?.bytes ?? [])) - } - - const { - code_generation_result: { - new_file_contents: newFiles = {}, - deleted_files: deletedFiles = [], - references = [], - }, - } = JSON.parse(new TextDecoder().decode(Buffer.from(buffer))) as { - // eslint-disable-next-line @typescript-eslint/naming-convention - code_generation_result: { - // eslint-disable-next-line @typescript-eslint/naming-convention - new_file_contents?: Record - // eslint-disable-next-line @typescript-eslint/naming-convention - deleted_files?: string[] - references?: CodeReference[] - } - } - UserWrittenCodeTracker.instance.onQFeatureInvoked() - - const newFileContents: { zipFilePath: string; fileContent: string }[] = [] - for (const [filePath, fileContent] of Object.entries(newFiles)) { - newFileContents.push({ zipFilePath: filePath, fileContent }) - } - - return { newFileContents, deletedFiles, references } - } catch (e) { - getLogger().error( - `${featureName}: failed to export archive result: ${(e as Error).message} RequestId: ${ - (e as any).requestId - }` - ) - - if (isAwsError(e)) { - throw ApiError.of(e.message, 'ExportResultArchive', e.code, e.statusCode ?? 500) - } - - throw new FeatureDevServiceError(e instanceof Error ? e.message : 'Unknown error', 'ExportResultArchive') - } - } - - /** - * This event is specific to ABTesting purposes. - * - * No need to fail currently if the event fails in the request. In addition, currently there is no need for a return value. - * - * @param conversationId - */ - public async sendFeatureDevTelemetryEvent(conversationId: string) { - await this.sendFeatureDevEvent('featureDevEvent', { - conversationId, - }) - } - - public async sendFeatureDevCodeGenerationEvent(event: FeatureDevCodeGenerationEvent) { - getLogger().debug( - `featureDevCodeGenerationEvent: conversationId: ${event.conversationId} charactersOfCodeGenerated: ${event.charactersOfCodeGenerated} linesOfCodeGenerated: ${event.linesOfCodeGenerated}` - ) - await this.sendFeatureDevEvent('featureDevCodeGenerationEvent', event) - } - - public async sendFeatureDevCodeAcceptanceEvent(event: FeatureDevCodeAcceptanceEvent) { - getLogger().debug( - `featureDevCodeAcceptanceEvent: conversationId: ${event.conversationId} charactersOfCodeAccepted: ${event.charactersOfCodeAccepted} linesOfCodeAccepted: ${event.linesOfCodeAccepted}` - ) - await this.sendFeatureDevEvent('featureDevCodeAcceptanceEvent', event) - } - - public async sendMetricData(event: MetricData) { - getLogger().debug(`featureDevCodeGenerationMetricData: dimensions: ${event.dimensions}`) - await this.sendFeatureDevEvent('metricData', event) - } - - public async sendFeatureDevEvent( - eventName: T, - event: NonNullable - ) { - try { - const client = await this.getClient() - const params: FeatureDevProxyClient.SendTelemetryEventRequest = { - telemetryEvent: { - [eventName]: event, - }, - optOutPreference: getOptOutPreference(), - userContext: { - ideCategory: 'VSCODE', - operatingSystem: getOperatingSystem(), - product: 'FeatureDev', // Should be the same as in JetBrains - clientId: getClientId(globals.globalState), - ideVersion: extensionVersion, - }, - profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn, - } - const response = await client.sendTelemetryEvent(params).promise() - getLogger().debug( - `${featureName}: successfully sent ${eventName} telemetryEvent:${'conversationId' in event ? ' ConversationId: ' + event.conversationId : ''} RequestId: ${response.$response.requestId}` - ) - } catch (e) { - getLogger().error( - `${featureName}: failed to send ${eventName} telemetry: ${(e as Error).name}: ${ - (e as Error).message - } RequestId: ${(e as any).requestId}` - ) - } - } -} diff --git a/packages/core/src/amazonqFeatureDev/constants.ts b/packages/core/src/amazonqFeatureDev/constants.ts deleted file mode 100644 index 78cae972cc3..00000000000 --- a/packages/core/src/amazonqFeatureDev/constants.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { CodeReference } from '../amazonq/webview/ui/connector' -import { LicenseUtil } from '../codewhisperer/util/licenseUtil' - -// The Scheme name of the virtual documents. -export const featureDevScheme = 'aws-featureDev' - -// For uniquely identifiying which chat messages should be routed to FeatureDev -export const featureDevChat = 'featureDevChat' - -export const featureName = 'Amazon Q Developer Agent for software development' - -export const generateDevFilePrompt = - "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported commands are install, build and test (are all optional). so you may have to bundle some commands together using '&&'. also you can use ”public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: install exec: component: dev commandLine: ”npm install” - id: build exec: component: dev commandLine: ”npm run build” - id: test exec: component: dev commandLine: ”npm run test”" - -// Max allowed size for file collection -export const maxRepoSizeBytes = 200 * 1024 * 1024 - -export const startCodeGenClientErrorMessages = ['Improperly formed request', 'Resource not found'] -export const startTaskAssistLimitReachedMessage = 'StartTaskAssistCodeGeneration reached for this month.' -export const clientErrorMessages = [ - 'The folder you chose did not contain any source files in a supported language. Choose another folder and try again.', -] - -// License text that's used in the file view -export const licenseText = (reference: CodeReference) => - `${ - reference.licenseName - } license from repository ${reference.repository}` diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts deleted file mode 100644 index bdf73eada07..00000000000 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ /dev/null @@ -1,1089 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatItemAction, MynahIcons } from '@aws/mynah-ui' -import * as path from 'path' -import * as vscode from 'vscode' -import { EventEmitter } from 'vscode' -import { telemetry } from '../../../shared/telemetry/telemetry' -import { createSingleFileDialog } from '../../../shared/ui/common/openDialog' -import { - CodeIterationLimitError, - ContentLengthError, - createUserFacingErrorMessage, - denyListedErrors, - FeatureDevServiceError, - getMetricResult, - MonthlyConversationLimitError, - NoChangeRequiredException, - PrepareRepoFailedError, - PromptRefusalException, - SelectedFolderNotInWorkspaceFolderError, - TabIdNotFoundError, - UploadCodeError, - UploadURLExpired, - UserMessageNotFoundError, - WorkspaceFolderNotFoundError, - ZipFileError, -} from '../../errors' -import { codeGenRetryLimit, defaultRetryLimit } from '../../limits' -import { Session } from '../../session/session' -import { featureDevScheme, featureName, generateDevFilePrompt } from '../../constants' -import { - DeletedFileInfo, - DevPhase, - MetricDataOperationName, - MetricDataResult, - type NewFileInfo, -} from '../../../amazonq/commons/types' -import { AuthUtil } from '../../../codewhisperer/util/authUtil' -import { AuthController } from '../../../amazonq/auth/controller' -import { getLogger } from '../../../shared/logger/logger' -import { submitFeedback } from '../../../feedback/vue/submitFeedback' -import { Commands, placeholder } from '../../../shared/vscode/commands2' -import { EditorContentController } from '../../../amazonq/commons/controllers/contentController' -import { openUrl } from '../../../shared/utilities/vsCodeUtils' -import { checkForDevFile, getPathsFromZipFilePath } from '../../../amazonq/util/files' -import { examples, messageWithConversationId } from '../../userFacingText' -import { getWorkspaceFoldersByPrefixes } from '../../../shared/utilities/workspaceUtils' -import { openDeletedDiff, openDiff } from '../../../amazonq/commons/diff' -import { i18n } from '../../../shared/i18n-helper' -import globals from '../../../shared/extensionGlobals' -import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings' -import { randomUUID } from '../../../shared/crypto' -import { FollowUpTypes } from '../../../amazonq/commons/types' -import { Messenger } from '../../../amazonq/commons/connector/baseMessenger' -import { BaseChatSessionStorage } from '../../../amazonq/commons/baseChatStorage' - -export interface ChatControllerEventEmitters { - readonly processHumanChatMessage: EventEmitter - readonly followUpClicked: EventEmitter - readonly openDiff: EventEmitter - readonly stopResponse: EventEmitter - readonly tabOpened: EventEmitter - readonly tabClosed: EventEmitter - readonly processChatItemVotedMessage: EventEmitter - readonly processChatItemFeedbackMessage: EventEmitter - readonly authClicked: EventEmitter - readonly processResponseBodyLinkClick: EventEmitter - readonly insertCodeAtPositionClicked: EventEmitter - readonly fileClicked: EventEmitter - readonly storeCodeResultMessageId: EventEmitter -} - -type OpenDiffMessage = { - tabID: string - messageId: string - // currently the zip file path - filePath: string - deleted: boolean - codeGenerationId: string -} - -type fileClickedMessage = { - tabID: string - messageId: string - filePath: string - actionName: string -} - -type StoreMessageIdMessage = { - tabID: string - messageId: string -} - -export class FeatureDevController { - private readonly scheme: string = featureDevScheme - private readonly messenger: Messenger - private readonly sessionStorage: BaseChatSessionStorage - private isAmazonQVisible: boolean - private authController: AuthController - private contentController: EditorContentController - - public constructor( - private readonly chatControllerMessageListeners: ChatControllerEventEmitters, - messenger: Messenger, - sessionStorage: BaseChatSessionStorage, - onDidChangeAmazonQVisibility: vscode.Event - ) { - this.messenger = messenger - this.sessionStorage = sessionStorage - this.authController = new AuthController() - this.contentController = new EditorContentController() - - /** - * defaulted to true because onDidChangeAmazonQVisibility doesn't get fire'd until after - * the view is opened - */ - this.isAmazonQVisible = true - - onDidChangeAmazonQVisibility((visible) => { - this.isAmazonQVisible = visible - }) - - this.chatControllerMessageListeners.processHumanChatMessage.event((data) => { - this.processUserChatMessage(data).catch((e) => { - getLogger().error('processUserChatMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.processChatItemVotedMessage.event((data) => { - this.processChatItemVotedMessage(data.tabID, data.vote).catch((e) => { - getLogger().error('processChatItemVotedMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.processChatItemFeedbackMessage.event((data) => { - this.processChatItemFeedbackMessage(data).catch((e) => { - getLogger().error('processChatItemFeedbackMessage failed: %s', (e as Error).message) - }) - }) - this.chatControllerMessageListeners.followUpClicked.event((data) => { - switch (data.followUp.type) { - case FollowUpTypes.InsertCode: - return this.insertCode(data) - case FollowUpTypes.ProvideFeedbackAndRegenerateCode: - return this.provideFeedbackAndRegenerateCode(data) - case FollowUpTypes.Retry: - return this.retryRequest(data) - case FollowUpTypes.ModifyDefaultSourceFolder: - return this.modifyDefaultSourceFolder(data) - case FollowUpTypes.DevExamples: - this.initialExamples(data) - break - case FollowUpTypes.NewTask: - this.messenger.sendAnswer({ - type: 'answer', - tabID: data?.tabID, - message: i18n('AWS.amazonq.featureDev.answer.newTaskChanges'), - }) - return this.newTask(data) - case FollowUpTypes.CloseSession: - return this.closeSession(data) - case FollowUpTypes.SendFeedback: - this.sendFeedback() - break - case FollowUpTypes.AcceptAutoBuild: - return this.processAutoBuildSetting(true, data) - case FollowUpTypes.DenyAutoBuild: - return this.processAutoBuildSetting(false, data) - case FollowUpTypes.GenerateDevFile: - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: data?.tabID, - message: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), - }) - return this.newTask(data, generateDevFilePrompt) - } - }) - this.chatControllerMessageListeners.openDiff.event((data) => { - return this.openDiff(data) - }) - this.chatControllerMessageListeners.stopResponse.event((data) => { - return this.stopResponse(data) - }) - this.chatControllerMessageListeners.tabOpened.event((data) => { - return this.tabOpened(data) - }) - this.chatControllerMessageListeners.tabClosed.event((data) => { - this.tabClosed(data) - }) - this.chatControllerMessageListeners.authClicked.event((data) => { - this.authClicked(data) - }) - this.chatControllerMessageListeners.processResponseBodyLinkClick.event((data) => { - this.processLink(data) - }) - this.chatControllerMessageListeners.insertCodeAtPositionClicked.event((data) => { - this.insertCodeAtPosition(data) - }) - this.chatControllerMessageListeners.fileClicked.event(async (data) => { - return await this.fileClicked(data) - }) - this.chatControllerMessageListeners.storeCodeResultMessageId.event(async (data) => { - return await this.storeCodeResultMessageId(data) - }) - AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(() => { - this.sessionStorage.deleteAllSessions() - }) - } - - private async processChatItemVotedMessage(tabId: string, vote: string) { - const session = await this.sessionStorage.getSession(tabId) - - if (vote === 'upvote') { - telemetry.amazonq_codeGenerationThumbsUp.emit({ - amazonqConversationId: session?.conversationId, - value: 1, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - } else if (vote === 'downvote') { - telemetry.amazonq_codeGenerationThumbsDown.emit({ - amazonqConversationId: session?.conversationId, - value: 1, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - } - } - - private async processChatItemFeedbackMessage(message: any) { - const session = await this.sessionStorage.getSession(message.tabId) - - await globals.telemetry.postFeedback({ - comment: `${JSON.stringify({ - type: 'featuredev-chat-answer-feedback', - conversationId: session?.conversationId ?? '', - messageId: message?.messageId, - reason: message?.selectedOption, - userComment: message?.comment, - })}`, - sentiment: 'Negative', // The chat UI reports only negative feedback currently. - }) - } - - private processErrorChatMessage = (err: any, message: any, session: Session | undefined) => { - const errorMessage = createUserFacingErrorMessage( - `${featureName} request failed: ${err.cause?.message ?? err.message}` - ) - - let defaultMessage - const isDenyListedError = denyListedErrors.some((denyListedError) => err.message.includes(denyListedError)) - - switch (err.constructor.name) { - case ContentLengthError.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message + messageWithConversationId(session?.conversationIdUnsafe), - canBeVoted: true, - }) - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: message.tabID, - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.modifyDefaultSourceFolder'), - type: 'ModifyDefaultSourceFolder', - status: 'info', - }, - ], - }) - break - case MonthlyConversationLimitError.name: - this.messenger.sendMonthlyLimitError(message.tabID) - break - case FeatureDevServiceError.name: - case UploadCodeError.name: - case UserMessageNotFoundError.name: - case TabIdNotFoundError.name: - case PrepareRepoFailedError.name: - this.messenger.sendErrorMessage( - errorMessage, - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - break - case PromptRefusalException.name: - case ZipFileError.name: - this.messenger.sendErrorMessage(errorMessage, message.tabID, 0, session?.conversationIdUnsafe, true) - break - case NoChangeRequiredException.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - canBeVoted: true, - }) - // Allow users to re-work the task description. - return this.newTask(message) - case CodeIterationLimitError.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message + messageWithConversationId(session?.conversationIdUnsafe), - canBeVoted: true, - }) - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: message.tabID, - followUps: [ - { - pillText: - session?.getInsertCodePillText([ - ...(session?.state.filePaths ?? []), - ...(session?.state.deletedFiles ?? []), - ]) ?? i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges'), - type: FollowUpTypes.InsertCode, - icon: 'ok' as MynahIcons, - status: 'success', - }, - ], - }) - break - case UploadURLExpired.name: - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - canBeVoted: true, - }) - break - default: - if (isDenyListedError || this.retriesRemaining(session) === 0) { - defaultMessage = i18n('AWS.amazonq.featureDev.error.codeGen.denyListedError') - } else { - defaultMessage = i18n('AWS.amazonq.featureDev.error.codeGen.default') - } - - this.messenger.sendErrorMessage( - defaultMessage ? defaultMessage : errorMessage, - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe, - !!defaultMessage - ) - - break - } - } - - /** - * - * This function dispose cancellation token to free resources and provide a new token. - * Since user can abort a call in the same session, when the processing ends, we need provide a new one - * to start with the new prompt and allow the ability to stop again. - * - * @param session - */ - - private disposeToken(session: Session | undefined) { - if (session?.state?.tokenSource?.token.isCancellationRequested) { - session?.state.tokenSource?.dispose() - if (session?.state?.tokenSource) { - session.state.tokenSource = new vscode.CancellationTokenSource() - } - getLogger().debug('Request cancelled, skipping further processing') - } - } - - // TODO add type - private async processUserChatMessage(message: any) { - if (message.message === undefined) { - this.messenger.sendErrorMessage('chatMessage should be set', message.tabID, 0, undefined) - return - } - - /** - * Don't attempt to process any chat messages when a workspace folder is not set. - * When the tab is first opened we will throw an error and lock the chat if the workspace - * folder is not found - */ - const workspaceFolders = vscode.workspace.workspaceFolders - if (workspaceFolders === undefined || workspaceFolders.length === 0) { - return - } - - let session - try { - getLogger().debug(`${featureName}: Processing message: ${message.message}`) - - session = await this.sessionStorage.getSession(message.tabID) - // set latestMessage in session as retry would lose context if function returns early - session.latestMessage = message.message - - await session.disableFileList() - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - await this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - - const root = session.getWorkspaceRoot() - const autoBuildProjectSetting = CodeWhispererSettings.instance.getAutoBuildSetting() - const hasDevfile = await checkForDevFile(root) - const isPromptedForAutoBuildFeature = Object.keys(autoBuildProjectSetting).includes(root) - - if (hasDevfile && !isPromptedForAutoBuildFeature) { - await this.promptAllowQCommandsConsent(message.tabID) - return - } - - await session.preloader() - - if (session.state.phase === DevPhase.CODEGEN) { - await this.onCodeGeneration(session, message.message, message.tabID) - } - } catch (err: any) { - this.disposeToken(session) - await this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) - } - } - - private async promptAllowQCommandsConsent(tabID: string) { - this.messenger.sendAnswer({ - tabID: tabID, - message: i18n('AWS.amazonq.featureDev.answer.devFileInRepository'), - type: 'answer', - }) - - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.acceptForProject'), - type: FollowUpTypes.AcceptAutoBuild, - status: 'success', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.declineForProject'), - type: FollowUpTypes.DenyAutoBuild, - status: 'error', - }, - ], - tabID: tabID, - }) - } - - /** - * Handle a regular incoming message when a user is in the code generation phase - */ - private async onCodeGeneration(session: Session, message: string, tabID: string) { - // lock the UI/show loading bubbles - this.messenger.sendAsyncEventProgress( - tabID, - true, - session.retries === codeGenRetryLimit - ? i18n('AWS.amazonq.featureDev.pillText.awaitMessage') - : i18n('AWS.amazonq.featureDev.pillText.awaitMessageRetry') - ) - - try { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.requestingChanges'), - type: 'answer-stream', - tabID, - canBeVoted: true, - }) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.generatingCode')) - await session.sendMetricDataTelemetry(MetricDataOperationName.StartCodeGeneration, MetricDataResult.Success) - await session.send(message) - const filePaths = session.state.filePaths ?? [] - const deletedFiles = session.state.deletedFiles ?? [] - // Only add the follow up accept/deny buttons when the tab hasn't been closed/request hasn't been cancelled - if (session?.state?.tokenSource?.token.isCancellationRequested) { - return - } - - if (filePaths.length === 0 && deletedFiles.length === 0) { - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.unableGenerateChanges'), - type: 'answer', - tabID: tabID, - canBeVoted: true, - }) - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID: tabID, - followUps: - this.retriesRemaining(session) > 0 - ? [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ] - : [], - }) - // Lock the chat input until they explicitly click retry - this.messenger.sendChatInputEnabled(tabID, false) - return - } - - this.messenger.sendCodeResult( - filePaths, - deletedFiles, - session.state.references ?? [], - tabID, - session.uploadId, - session.state.codeGenerationId ?? '' - ) - - const remainingIterations = session.state.codeGenerationRemainingIterationCount - const totalIterations = session.state.codeGenerationTotalIterationCount - - if (remainingIterations !== undefined && totalIterations !== undefined) { - this.messenger.sendAnswer({ - type: 'answer' as const, - tabID: tabID, - message: (() => { - if (remainingIterations > 2) { - return 'Would you like me to add this code to your project, or provide feedback for new code?' - } else if (remainingIterations > 0) { - return `Would you like me to add this code to your project, or provide feedback for new code? You have ${remainingIterations} out of ${totalIterations} code generations left.` - } else { - return 'Would you like me to add this code to your project?' - } - })(), - }) - } - - if (session?.state.phase === DevPhase.CODEGEN) { - const messageId = randomUUID() - session.updateAcceptCodeMessageId(messageId) - session.updateAcceptCodeTelemetrySent(false) - // need to add the followUps with an extra update here, or it will double-render them - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [], - tabID: tabID, - messageId, - }) - await session.updateChatAnswer(tabID, i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges')) - await session.sendLinesOfCodeGeneratedTelemetry() - } - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption')) - } catch (err: any) { - getLogger().error(`${featureName}: Error during code generation: ${err}`) - await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, getMetricResult(err)) - throw err - } finally { - // Finish processing the event - - if (session?.state?.tokenSource?.token.isCancellationRequested) { - await this.workOnNewTask( - session.tabID, - session.state.codeGenerationRemainingIterationCount, - session.state.codeGenerationTotalIterationCount, - session?.state?.tokenSource?.token.isCancellationRequested - ) - this.disposeToken(session) - } else { - this.messenger.sendAsyncEventProgress(tabID, false, undefined) - - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(tabID, false) - - if (!this.isAmazonQVisible) { - const open = 'Open chat' - const resp = await vscode.window.showInformationMessage( - i18n('AWS.amazonq.featureDev.answer.qGeneratedCode'), - open - ) - if (resp === open) { - await Commands.tryExecute('aws.amazonq.AmazonQChatView.focus') - // TODO add focusing on the specific tab once that's implemented - } - } - } - } - await session.sendMetricDataTelemetry(MetricDataOperationName.EndCodeGeneration, MetricDataResult.Success) - } - - private sendUpdateCodeMessage(tabID: string) { - this.messenger.sendAnswer({ - type: 'answer', - tabID, - message: i18n('AWS.amazonq.featureDev.answer.updateCode'), - canBeVoted: true, - }) - } - - private async workOnNewTask( - tabID: string, - remainingIterations: number = 0, - totalIterations?: number, - isStoppedGeneration: boolean = false - ) { - const hasDevFile = await checkForDevFile((await this.sessionStorage.getSession(tabID)).getWorkspaceRoot()) - - if (isStoppedGeneration) { - this.messenger.sendAnswer({ - message: ((remainingIterations) => { - if (totalIterations !== undefined) { - if (remainingIterations <= 0) { - return "I stopped generating your code. You don't have more iterations left, however, you can start a new session." - } else if (remainingIterations <= 2) { - return `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remainingIterations} out of ${totalIterations} code generations left.` - } - } - return 'I stopped generating your code. If you want to continue working on this task, provide another description.' - })(remainingIterations), - type: 'answer-part', - tabID, - }) - } - - if ((remainingIterations <= 0 && isStoppedGeneration) || !isStoppedGeneration) { - const followUps: Array = [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ] - - if (!hasDevFile) { - followUps.push({ - pillText: i18n('AWS.amazonq.featureDev.pillText.generateDevFile'), - type: FollowUpTypes.GenerateDevFile, - status: 'info', - }) - - this.messenger.sendAnswer({ - type: 'answer', - tabID, - message: i18n('AWS.amazonq.featureDev.answer.devFileSuggestion'), - }) - } - - this.messenger.sendAnswer({ - type: 'system-prompt', - tabID, - followUps, - }) - this.messenger.sendChatInputEnabled(tabID, false) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.pillText.selectOption')) - return - } - - // Ensure that chat input is enabled so that they can provide additional iterations if they choose - this.messenger.sendChatInputEnabled(tabID, true) - this.messenger.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.additionalImprovements')) - } - - private async processAutoBuildSetting(setting: boolean, msg: any) { - const root = (await this.sessionStorage.getSession(msg.tabID)).getWorkspaceRoot() - await CodeWhispererSettings.instance.updateAutoBuildSetting(root, setting) - - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.answer.settingUpdated'), - tabID: msg.tabID, - type: 'answer', - }) - - await this.retryRequest(msg) - } - - // TODO add type - private async insertCode(message: any) { - let session - try { - session = await this.sessionStorage.getSession(message.tabID) - - const acceptedFiles = (paths?: { rejected: boolean }[]) => (paths || []).filter((i) => !i.rejected).length - - const filesAccepted = acceptedFiles(session.state.filePaths) + acceptedFiles(session.state.deletedFiles) - - this.sendAcceptCodeTelemetry(session, filesAccepted) - - await session.insertChanges() - - if (session.acceptCodeMessageId) { - this.sendUpdateCodeMessage(message.tabID) - await this.workOnNewTask( - message.tabID, - session.state.codeGenerationRemainingIterationCount, - session.state.codeGenerationTotalIterationCount - ) - await this.clearAcceptCodeMessageId(message.tabID) - } - } catch (err: any) { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(`Failed to insert code changes: ${err.message}`), - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - } - } - - private async provideFeedbackAndRegenerateCode(message: any) { - const session = await this.sessionStorage.getSession(message.tabID) - telemetry.amazonq_isProvideFeedbackForCodeGen.emit({ - amazonqConversationId: session.conversationId, - enabled: true, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - // Unblock the message button - this.messenger.sendAsyncEventProgress(message.tabID, false, undefined) - - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.answer.howCodeCanBeImproved'), - canBeVoted: true, - }) - - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.feedback')) - } - - private async retryRequest(message: any) { - let session - try { - this.messenger.sendAsyncEventProgress(message.tabID, true, undefined) - - session = await this.sessionStorage.getSession(message.tabID) - - // Decrease retries before making this request, just in case this one fails as well - session.decreaseRetries() - - // Sending an empty message will re-run the last state with the previous values - await this.processUserChatMessage({ - message: session.latestMessage, - tabID: message.tabID, - }) - } catch (err: any) { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(`Failed to retry request: ${err.message}`), - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - } finally { - // Finish processing the event - this.messenger.sendAsyncEventProgress(message.tabID, false, undefined) - } - } - - private async modifyDefaultSourceFolder(message: any) { - const session = await this.sessionStorage.getSession(message.tabID) - - const uri = await createSingleFileDialog({ - canSelectFolders: true, - canSelectFiles: false, - }).prompt() - - let metricData: { result: 'Succeeded' } | { result: 'Failed'; reason: string } | undefined - - if (!(uri instanceof vscode.Uri)) { - this.messenger.sendAnswer({ - tabID: message.tabID, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.selectFiles'), - type: 'ModifyDefaultSourceFolder', - status: 'info', - }, - ], - }) - metricData = { result: 'Failed', reason: 'ClosedBeforeSelection' } - } else if (!vscode.workspace.getWorkspaceFolder(uri)) { - this.messenger.sendAnswer({ - tabID: message.tabID, - type: 'answer', - message: new SelectedFolderNotInWorkspaceFolderError().message, - canBeVoted: true, - }) - this.messenger.sendAnswer({ - tabID: message.tabID, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.selectFiles'), - type: 'ModifyDefaultSourceFolder', - status: 'info', - }, - ], - }) - metricData = { result: 'Failed', reason: 'NotInWorkspaceFolder' } - } else { - session.updateWorkspaceRoot(uri.fsPath) - metricData = { result: 'Succeeded' } - this.messenger.sendAnswer({ - message: `Changed source root to: ${uri.fsPath}`, - type: 'answer', - tabID: message.tabID, - canBeVoted: true, - }) - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ], - tabID: message.tabID, - }) - this.messenger.sendChatInputEnabled(message.tabID, true) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.pillText.writeNewPrompt')) - } - - telemetry.amazonq_modifySourceFolder.emit({ - credentialStartUrl: AuthUtil.instance.startUrl, - amazonqConversationId: session.conversationId, - ...metricData, - }) - } - - private initialExamples(message: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: examples, - canBeVoted: true, - }) - } - - private async fileClicked(message: fileClickedMessage) { - // TODO: add Telemetry here - const tabId: string = message.tabID - const messageId = message.messageId - const filePathToUpdate: string = message.filePath - const action = message.actionName - - const session = await this.sessionStorage.getSession(tabId) - const filePathIndex = (session.state.filePaths ?? []).findIndex((obj) => obj.relativePath === filePathToUpdate) - const deletedFilePathIndex = (session.state.deletedFiles ?? []).findIndex( - (obj) => obj.relativePath === filePathToUpdate - ) - - if (filePathIndex !== -1 && session.state.filePaths) { - if (action === 'accept-change') { - this.sendAcceptCodeTelemetry(session, 1) - await session.insertNewFiles([session.state.filePaths[filePathIndex]]) - await session.insertCodeReferenceLogs(session.state.references ?? []) - await this.openFile(session.state.filePaths[filePathIndex], tabId) - } else { - session.state.filePaths[filePathIndex].rejected = !session.state.filePaths[filePathIndex].rejected - } - } - if (deletedFilePathIndex !== -1 && session.state.deletedFiles) { - if (action === 'accept-change') { - this.sendAcceptCodeTelemetry(session, 1) - await session.applyDeleteFiles([session.state.deletedFiles[deletedFilePathIndex]]) - await session.insertCodeReferenceLogs(session.state.references ?? []) - } else { - session.state.deletedFiles[deletedFilePathIndex].rejected = - !session.state.deletedFiles[deletedFilePathIndex].rejected - } - } - - await session.updateFilesPaths({ - tabID: tabId, - filePaths: session.state.filePaths ?? [], - deletedFiles: session.state.deletedFiles ?? [], - messageId, - }) - - if (session.acceptCodeMessageId) { - const allFilePathsAccepted = session.state.filePaths?.every( - (filePath: NewFileInfo) => !filePath.rejected && filePath.changeApplied - ) - const allDeletedFilePathsAccepted = session.state.deletedFiles?.every( - (filePath: DeletedFileInfo) => !filePath.rejected && filePath.changeApplied - ) - if (allFilePathsAccepted && allDeletedFilePathsAccepted) { - this.sendUpdateCodeMessage(tabId) - await this.workOnNewTask( - tabId, - session.state.codeGenerationRemainingIterationCount, - session.state.codeGenerationTotalIterationCount - ) - await this.clearAcceptCodeMessageId(tabId) - } - } - } - - private async storeCodeResultMessageId(message: StoreMessageIdMessage) { - const tabId: string = message.tabID - const messageId = message.messageId - const session = await this.sessionStorage.getSession(tabId) - - session.updateCodeResultMessageId(messageId) - } - - private async openDiff(message: OpenDiffMessage) { - const tabId: string = message.tabID - const codeGenerationId: string = message.messageId - const zipFilePath: string = message.filePath - const session = await this.sessionStorage.getSession(tabId) - telemetry.amazonq_isReviewedChanges.emit({ - amazonqConversationId: session.conversationId, - enabled: true, - result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.startUrl, - }) - - const workspacePrefixMapping = getWorkspaceFoldersByPrefixes(session.config.workspaceFolders) - const pathInfos = getPathsFromZipFilePath(zipFilePath, workspacePrefixMapping, session.config.workspaceFolders) - - if (message.deleted) { - const name = path.basename(pathInfos.relativePath) - await openDeletedDiff(pathInfos.absolutePath, name, tabId, this.scheme) - } else { - let uploadId = session.uploadId - if (session?.state?.uploadHistory && session.state.uploadHistory[codeGenerationId]) { - uploadId = session?.state?.uploadHistory[codeGenerationId].uploadId - } - const rightPath = path.join(uploadId, zipFilePath) - await openDiff(pathInfos.absolutePath, rightPath, tabId, this.scheme) - } - } - - private async openFile(filePath: NewFileInfo, tabId: string) { - const leftPath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - const rightPath = filePath.virtualMemoryUri.path - await openDiff(leftPath, rightPath, tabId, this.scheme) - } - - private async stopResponse(message: any) { - telemetry.ui_click.emit({ elementId: 'amazonq_stopCodeGeneration' }) - this.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration'), - type: 'answer-part', - tabID: message.tabID, - }) - this.messenger.sendUpdatePlaceholder( - message.tabID, - i18n('AWS.amazonq.featureDev.pillText.stoppingCodeGeneration') - ) - this.messenger.sendChatInputEnabled(message.tabID, false) - - const session = await this.sessionStorage.getSession(message.tabID) - if (session.state?.tokenSource) { - session.state?.tokenSource?.cancel() - } - } - - private async tabOpened(message: any) { - let session: Session | undefined - try { - session = await this.sessionStorage.getSession(message.tabID) - getLogger().debug(`${featureName}: Session created with id: ${session.tabID}`) - - const authState = await AuthUtil.instance.getChatAuthState() - if (authState.amazonQ !== 'connected') { - void this.messenger.sendAuthNeededExceptionMessage(authState, message.tabID) - session.isAuthenticating = true - return - } - } catch (err: any) { - if (err instanceof WorkspaceFolderNotFoundError) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: err.message, - }) - this.messenger.sendChatInputEnabled(message.tabID, false) - } else { - this.messenger.sendErrorMessage( - createUserFacingErrorMessage(err.message), - message.tabID, - this.retriesRemaining(session), - session?.conversationIdUnsafe - ) - } - } - } - - private authClicked(message: any) { - this.authController.handleAuth(message.authType) - - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.pillText.reauthenticate'), - }) - - // Explicitly ensure the user goes through the re-authenticate flow - this.messenger.sendChatInputEnabled(message.tabID, false) - } - - private tabClosed(message: any) { - this.sessionStorage.deleteSession(message.tabID) - } - - private async newTask(message: any, prefilledPrompt?: string) { - // Old session for the tab is ending, delete it so we can create a new one for the message id - const session = await this.sessionStorage.getSession(message.tabID) - await session.disableFileList() - telemetry.amazonq_endChat.emit({ - amazonqConversationId: session.conversationId, - amazonqEndOfTheConversationLatency: performance.now() - session.telemetry.sessionStartTime, - result: 'Succeeded', - }) - this.sessionStorage.deleteSession(message.tabID) - - // Re-run the opening flow, where we check auth + create a session - await this.tabOpened(message) - - if (prefilledPrompt) { - await this.processUserChatMessage({ ...message, message: prefilledPrompt }) - } else { - this.messenger.sendChatInputEnabled(message.tabID, true) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.describe')) - } - } - - private async closeSession(message: any) { - this.messenger.sendAnswer({ - type: 'answer', - tabID: message.tabID, - message: i18n('AWS.amazonq.featureDev.answer.sessionClosed'), - }) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.featureDev.placeholder.sessionClosed')) - this.messenger.sendChatInputEnabled(message.tabID, false) - - const session = await this.sessionStorage.getSession(message.tabID) - await session.disableFileList() - telemetry.amazonq_endChat.emit({ - amazonqConversationId: session.conversationId, - amazonqEndOfTheConversationLatency: performance.now() - session.telemetry.sessionStartTime, - result: 'Succeeded', - }) - } - - private sendFeedback() { - void submitFeedback(placeholder, 'Amazon Q') - } - - private processLink(message: any) { - void openUrl(vscode.Uri.parse(message.link)) - } - - private insertCodeAtPosition(message: any) { - this.contentController.insertTextAtCursorPosition(message.code, () => {}) - } - - private retriesRemaining(session: Session | undefined) { - return session?.retries ?? defaultRetryLimit - } - - private async clearAcceptCodeMessageId(tabID: string) { - const session = await this.sessionStorage.getSession(tabID) - session.updateAcceptCodeMessageId(undefined) - } - - private sendAcceptCodeTelemetry(session: Session, amazonqNumberOfFilesAccepted: number) { - // accepted code telemetry is only to be sent once per iteration of code generation - if (amazonqNumberOfFilesAccepted > 0 && !session.acceptCodeTelemetrySent) { - session.updateAcceptCodeTelemetrySent(true) - telemetry.amazonq_isAcceptedCodeChanges.emit({ - credentialStartUrl: AuthUtil.instance.startUrl, - amazonqConversationId: session.conversationId, - amazonqNumberOfFilesAccepted, - enabled: true, - result: 'Succeeded', - }) - } - } -} diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/constants.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/constants.ts deleted file mode 100644 index 086096b68a2..00000000000 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export type MessengerTypes = 'answer' | 'answer-part' | 'answer-stream' | 'system-prompt' diff --git a/packages/core/src/amazonqFeatureDev/errors.ts b/packages/core/src/amazonqFeatureDev/errors.ts deleted file mode 100644 index 2eb142f765b..00000000000 --- a/packages/core/src/amazonqFeatureDev/errors.ts +++ /dev/null @@ -1,191 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { featureName, clientErrorMessages, startTaskAssistLimitReachedMessage } from './constants' -import { uploadCodeError } from './userFacingText' -import { i18n } from '../shared/i18n-helper' -import { LlmError } from '../amazonq/errors' -import { MetricDataResult } from '../amazonq/commons/types' -import { - ClientError, - ServiceError, - ContentLengthError as CommonContentLengthError, - ToolkitError, -} from '../shared/errors' - -export class ConversationIdNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.conversationIdNotFoundError'), { - code: 'ConversationIdNotFound', - }) - } -} - -export class TabIdNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.tabIdNotFoundError'), { - code: 'TabIdNotFound', - }) - } -} - -export class WorkspaceFolderNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.workspaceFolderNotFoundError'), { - code: 'WorkspaceFolderNotFound', - }) - } -} - -export class UserMessageNotFoundError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.userMessageNotFoundError'), { - code: 'MessageNotFound', - }) - } -} - -export class SelectedFolderNotInWorkspaceFolderError extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.selectedFolderNotInWorkspaceFolderError'), { - code: 'SelectedFolderNotInWorkspaceFolder', - }) - } -} - -export class PromptRefusalException extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.promptRefusalException'), { - code: 'PromptRefusalException', - }) - } -} - -export class NoChangeRequiredException extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.noChangeRequiredException'), { - code: 'NoChangeRequiredException', - }) - } -} - -export class FeatureDevServiceError extends ServiceError { - constructor(message: string, code: string) { - super(message, { code }) - } -} - -export class PrepareRepoFailedError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.prepareRepoFailedError'), { - code: 'PrepareRepoFailed', - }) - } -} - -export class UploadCodeError extends ServiceError { - constructor(statusCode: string) { - super(uploadCodeError, { code: `UploadCode-${statusCode}` }) - } -} - -export class UploadURLExpired extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.uploadURLExpired'), { code: 'UploadURLExpired' }) - } -} - -export class IllegalStateTransition extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.illegalStateTransition'), { code: 'IllegalStateTransition' }) - } -} - -export class IllegalStateError extends ServiceError { - constructor(message: string) { - super(message, { code: 'IllegalStateTransition' }) - } -} - -export class ContentLengthError extends CommonContentLengthError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.name }) - } -} - -export class ZipFileError extends ServiceError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.name }) - } -} - -export class CodeIterationLimitError extends ClientError { - constructor() { - super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.name }) - } -} - -export class MonthlyConversationLimitError extends ClientError { - constructor(message: string) { - super(message, { code: MonthlyConversationLimitError.name }) - } -} - -export class UnknownApiError extends ServiceError { - constructor(message: string, api: string) { - super(message, { code: `${api}-Unknown` }) - } -} - -export class ApiClientError extends ClientError { - constructor(message: string, api: string, errorName: string, errorCode: number) { - super(message, { code: `${api}-${errorName}-${errorCode}` }) - } -} - -export class ApiServiceError extends ServiceError { - constructor(message: string, api: string, errorName: string, errorCode: number) { - super(message, { code: `${api}-${errorName}-${errorCode}` }) - } -} - -export class ApiError { - static of(message: string, api: string, errorName: string, errorCode: number) { - if (errorCode >= 400 && errorCode < 500) { - return new ApiClientError(message, api, errorName, errorCode) - } - return new ApiServiceError(message, api, errorName, errorCode) - } -} - -export const denyListedErrors: string[] = ['Deserialization error', 'Inaccessible host'] - -export function createUserFacingErrorMessage(message: string) { - if (denyListedErrors.some((err) => message.includes(err))) { - return `${featureName} API request failed` - } - return message -} - -function isAPIClientError(error: { code?: string; message: string }): boolean { - return ( - clientErrorMessages.some((msg: string) => error.message.includes(msg)) || - error.message.includes(startTaskAssistLimitReachedMessage) - ) -} - -export function getMetricResult(error: ToolkitError): MetricDataResult { - if (error instanceof ClientError || isAPIClientError(error)) { - return MetricDataResult.Error - } - if (error instanceof ServiceError) { - return MetricDataResult.Fault - } - if (error instanceof LlmError) { - return MetricDataResult.LlmFailure - } - - return MetricDataResult.Fault -} diff --git a/packages/core/src/amazonqFeatureDev/index.ts b/packages/core/src/amazonqFeatureDev/index.ts deleted file mode 100644 index 55114de0a06..00000000000 --- a/packages/core/src/amazonqFeatureDev/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './userFacingText' -export * from './errors' -export * from './session/sessionState' -export * from './constants' -export { Session } from './session/session' -export { FeatureDevClient } from './client/featureDev' -export { FeatureDevChatSessionStorage } from './storages/chatSession' -export { TelemetryHelper } from '../amazonq/util/telemetryHelper' -export { prepareRepoData, PrepareRepoDataOptions } from '../amazonq/util/files' -export { ChatControllerEventEmitters, FeatureDevController } from './controllers/chat/controller' diff --git a/packages/core/src/amazonqFeatureDev/limits.ts b/packages/core/src/amazonqFeatureDev/limits.ts deleted file mode 100644 index 04b677aaa0f..00000000000 --- a/packages/core/src/amazonqFeatureDev/limits.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// Max number of times a user can attempt to retry a codegen request if it fails -export const codeGenRetryLimit = 3 - -// The default retry limit used when the session could not be found -export const defaultRetryLimit = 0 - -// The max size a file that is uploaded can be -// 1024 KB -export const maxFileSizeBytes = 1024000 diff --git a/packages/core/src/amazonqFeatureDev/models.ts b/packages/core/src/amazonqFeatureDev/models.ts deleted file mode 100644 index ad37c01e477..00000000000 --- a/packages/core/src/amazonqFeatureDev/models.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export interface IManifestFile { - pomArtifactId: string - pomFolderName: string - hilCapability: string - pomGroupId: string - sourcePomVersion: string -} diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts deleted file mode 100644 index c1fc81a4701..00000000000 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ /dev/null @@ -1,412 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as path from 'path' - -import { ConversationNotStartedState, FeatureDevPrepareCodeGenState } from './sessionState' -import { - type DeletedFileInfo, - type Interaction, - type NewFileInfo, - type SessionState, - type SessionStateConfig, - UpdateFilesPathsParams, -} from '../../amazonq/commons/types' -import { ContentLengthError, ConversationIdNotFoundError, IllegalStateError } from '../errors' -import { featureDevChat, featureDevScheme } from '../constants' -import fs from '../../shared/fs/fs' -import { FeatureDevClient } from '../client/featureDev' -import { codeGenRetryLimit } from '../limits' -import { telemetry } from '../../shared/telemetry/telemetry' -import { TelemetryHelper } from '../../amazonq/util/telemetryHelper' -import { ReferenceLogViewProvider } from '../../codewhisperer/service/referenceLogViewProvider' -import { AuthUtil } from '../../codewhisperer/util/authUtil' -import { getLogger } from '../../shared/logger/logger' -import { logWithConversationId } from '../userFacingText' -import { CodeReference } from '../../amazonq/webview/ui/connector' -import { MynahIcons } from '@aws/mynah-ui' -import { i18n } from '../../shared/i18n-helper' -import { computeDiff } from '../../amazonq/commons/diff' -import { UpdateAnswerMessage } from '../../amazonq/commons/connector/connectorMessages' -import { FollowUpTypes } from '../../amazonq/commons/types' -import { SessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { ContentLengthError as CommonContentLengthError } from '../../shared/errors' -import { referenceLogText } from '../../amazonq/commons/model' - -export class Session { - private _state?: SessionState | Omit - private task: string = '' - private proxyClient: FeatureDevClient - private _conversationId?: string - private codeGenRetries: number - private preloaderFinished = false - private _latestMessage: string = '' - private _telemetry: TelemetryHelper - private _codeResultMessageId: string | undefined = undefined - private _acceptCodeMessageId: string | undefined = undefined - private _acceptCodeTelemetrySent = false - private _reportedCodeChanges: Set - - // Used to keep track of whether or not the current session is currently authenticating/needs authenticating - public isAuthenticating: boolean - - constructor( - public readonly config: SessionConfig, - private messenger: Messenger, - public readonly tabID: string, - initialState: Omit = new ConversationNotStartedState(tabID), - proxyClient: FeatureDevClient = new FeatureDevClient() - ) { - this._state = initialState - this.proxyClient = proxyClient - - this.codeGenRetries = codeGenRetryLimit - - this._telemetry = new TelemetryHelper() - this.isAuthenticating = false - this._reportedCodeChanges = new Set() - } - - /** - * Preload any events that have to run before a chat message can be sent - */ - async preloader() { - if (!this.preloaderFinished) { - await this.setupConversation() - this.preloaderFinished = true - this.messenger.sendAsyncEventProgress(this.tabID, true, undefined) - await this.proxyClient.sendFeatureDevTelemetryEvent(this.conversationId) // send the event only once per conversation. - } - } - - /** - * setupConversation - * - * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it. - */ - private async setupConversation() { - await telemetry.amazonq_startConversationInvoke.run(async (span) => { - this._conversationId = await this.proxyClient.createConversation() - getLogger().info(logWithConversationId(this.conversationId)) - - span.record({ amazonqConversationId: this._conversationId, credentialStartUrl: AuthUtil.instance.startUrl }) - }) - - this._state = new FeatureDevPrepareCodeGenState( - { - ...this.getSessionStateConfig(), - conversationId: this.conversationId, - uploadId: '', - currentCodeGenerationId: undefined, - }, - [], - [], - [], - this.tabID, - 0 - ) - } - - updateWorkspaceRoot(workspaceRootFolder: string) { - this.config.workspaceRoots = [workspaceRootFolder] - this._state && this._state.updateWorkspaceRoot && this._state.updateWorkspaceRoot(workspaceRootFolder) - } - - getWorkspaceRoot(): string { - return this.config.workspaceRoots[0] - } - - private getSessionStateConfig(): Omit { - return { - workspaceRoots: this.config.workspaceRoots, - workspaceFolders: this.config.workspaceFolders, - proxyClient: this.proxyClient, - conversationId: this.conversationId, - } - } - - async send(msg: string): Promise { - // When the task/"thing to do" hasn't been set yet, we want it to be the incoming message - if (this.task === '' && msg) { - this.task = msg - } - - this._latestMessage = msg - - return this.nextInteraction(msg) - } - - private async nextInteraction(msg: string) { - try { - const resp = await this.state.interact({ - task: this.task, - msg, - fs: this.config.fs, - messenger: this.messenger, - telemetry: this.telemetry, - tokenSource: this.state.tokenSource, - uploadHistory: this.state.uploadHistory, - }) - - if (resp.nextState) { - if (!this.state?.tokenSource?.token.isCancellationRequested) { - this.state?.tokenSource?.cancel() - } - // Move to the next state - this._state = resp.nextState - } - - return resp.interaction - } catch (e) { - if (e instanceof CommonContentLengthError) { - getLogger().debug(`Content length validation failed: ${e.message}`) - throw new ContentLengthError() - } - throw e - } - } - - public async updateFilesPaths(params: UpdateFilesPathsParams) { - const { tabID, filePaths, deletedFiles, messageId, disableFileActions = false } = params - this.messenger.updateFileComponent(tabID, filePaths, deletedFiles, messageId, disableFileActions) - await this.updateChatAnswer(tabID, this.getInsertCodePillText([...filePaths, ...deletedFiles])) - } - - public async updateChatAnswer(tabID: string, insertCodePillText: string) { - if (this._acceptCodeMessageId) { - const answer = new UpdateAnswerMessage( - { - messageId: this._acceptCodeMessageId, - messageType: 'system-prompt', - followUps: [ - { - pillText: insertCodePillText, - type: FollowUpTypes.InsertCode, - icon: 'ok' as MynahIcons, - status: 'success', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.provideFeedback'), - type: FollowUpTypes.ProvideFeedbackAndRegenerateCode, - icon: 'refresh' as MynahIcons, - status: 'info', - }, - ], - }, - tabID, - featureDevChat - ) - this.messenger.updateChatAnswer(answer) - } - } - - public async insertChanges() { - const newFilePaths = - this.state.filePaths?.filter((filePath) => !filePath.rejected && !filePath.changeApplied) ?? [] - await this.insertNewFiles(newFilePaths) - - const deletedFiles = - this.state.deletedFiles?.filter((deletedFile) => !deletedFile.rejected && !deletedFile.changeApplied) ?? [] - await this.applyDeleteFiles(deletedFiles) - - await this.insertCodeReferenceLogs(this.state.references ?? []) - - if (this._codeResultMessageId) { - await this.updateFilesPaths({ - tabID: this.state.tabID, - filePaths: this.state.filePaths ?? [], - deletedFiles: this.state.deletedFiles ?? [], - messageId: this._codeResultMessageId, - }) - } - } - - public async insertNewFiles(newFilePaths: NewFileInfo[]) { - await this.sendLinesOfCodeAcceptedTelemetry(newFilePaths) - for (const filePath of newFilePaths) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - - const uri = filePath.virtualMemoryUri - const content = await this.config.fs.readFile(uri) - const decodedContent = new TextDecoder().decode(content) - - await fs.mkdir(path.dirname(absolutePath)) - await fs.writeFile(absolutePath, decodedContent) - filePath.changeApplied = true - } - } - - public async applyDeleteFiles(deletedFiles: DeletedFileInfo[]) { - for (const filePath of deletedFiles) { - const absolutePath = path.join(filePath.workspaceFolder.uri.fsPath, filePath.relativePath) - await fs.delete(absolutePath) - filePath.changeApplied = true - } - } - - public async insertCodeReferenceLogs(codeReferences: CodeReference[]) { - for (const ref of codeReferences) { - ReferenceLogViewProvider.instance.addReferenceLog(referenceLogText(ref)) - } - } - - public async disableFileList() { - if (this._codeResultMessageId === undefined) { - return - } - - await this.updateFilesPaths({ - tabID: this.state.tabID, - filePaths: this.state.filePaths ?? [], - deletedFiles: this.state.deletedFiles ?? [], - messageId: this._codeResultMessageId, - disableFileActions: true, - }) - this._codeResultMessageId = undefined - } - - public updateCodeResultMessageId(messageId?: string) { - this._codeResultMessageId = messageId - } - - public updateAcceptCodeMessageId(messageId?: string) { - this._acceptCodeMessageId = messageId - } - - public updateAcceptCodeTelemetrySent(sent: boolean) { - this._acceptCodeTelemetrySent = sent - } - - public getInsertCodePillText(files: Array) { - if (files.every((file) => file.rejected || file.changeApplied)) { - return i18n('AWS.amazonq.featureDev.pillText.continue') - } - if (files.some((file) => file.rejected || file.changeApplied)) { - return i18n('AWS.amazonq.featureDev.pillText.acceptRemainingChanges') - } - return i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges') - } - - public async computeFilePathDiff(filePath: NewFileInfo) { - const leftPath = `${filePath.workspaceFolder.uri.fsPath}/${filePath.relativePath}` - const rightPath = filePath.virtualMemoryUri.path - const diff = await computeDiff(leftPath, rightPath, this.tabID, featureDevScheme) - return { leftPath, rightPath, ...diff } - } - - public async sendMetricDataTelemetry(operationName: string, result: string) { - await this.proxyClient.sendMetricData({ - metricName: 'Operation', - metricValue: 1, - timestamp: new Date(), - product: 'FeatureDev', - dimensions: [ - { - name: 'operationName', - value: operationName, - }, - { - name: 'result', - value: result, - }, - ], - }) - } - - public async sendLinesOfCodeGeneratedTelemetry() { - let charactersOfCodeGenerated = 0 - let linesOfCodeGenerated = 0 - // deleteFiles are currently not counted because the number of lines added is always 0 - const filePaths = this.state.filePaths ?? [] - for (const filePath of filePaths) { - const { leftPath, changes, charsAdded, linesAdded } = await this.computeFilePathDiff(filePath) - const codeChangeKey = `${leftPath}#@${JSON.stringify(changes)}` - if (this._reportedCodeChanges.has(codeChangeKey)) { - continue - } - charactersOfCodeGenerated += charsAdded - linesOfCodeGenerated += linesAdded - this._reportedCodeChanges.add(codeChangeKey) - } - await this.proxyClient.sendFeatureDevCodeGenerationEvent({ - conversationId: this.conversationId, - charactersOfCodeGenerated, - linesOfCodeGenerated, - }) - } - - public async sendLinesOfCodeAcceptedTelemetry(filePaths: NewFileInfo[]) { - let charactersOfCodeAccepted = 0 - let linesOfCodeAccepted = 0 - for (const filePath of filePaths) { - const { charsAdded, linesAdded } = await this.computeFilePathDiff(filePath) - charactersOfCodeAccepted += charsAdded - linesOfCodeAccepted += linesAdded - } - await this.proxyClient.sendFeatureDevCodeAcceptanceEvent({ - conversationId: this.conversationId, - charactersOfCodeAccepted, - linesOfCodeAccepted, - }) - } - - get state() { - if (!this._state) { - throw new IllegalStateError("State should be initialized before it's read") - } - return this._state - } - - get currentCodeGenerationId() { - return this.state.currentCodeGenerationId - } - - get uploadId() { - if (!('uploadId' in this.state)) { - throw new IllegalStateError("UploadId has to be initialized before it's read") - } - return this.state.uploadId - } - - get retries() { - return this.codeGenRetries - } - - decreaseRetries() { - this.codeGenRetries -= 1 - } - get conversationId() { - if (!this._conversationId) { - throw new ConversationIdNotFoundError() - } - return this._conversationId - } - - // Used for cases where it is not needed to have conversationId - get conversationIdUnsafe() { - return this._conversationId - } - - get latestMessage() { - return this._latestMessage - } - - set latestMessage(msg: string) { - this._latestMessage = msg - } - - get telemetry() { - return this._telemetry - } - - get acceptCodeMessageId() { - return this._acceptCodeMessageId - } - - get acceptCodeTelemetrySent() { - return this._acceptCodeTelemetrySent - } -} diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts deleted file mode 100644 index 5879c16493f..00000000000 --- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts +++ /dev/null @@ -1,285 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { MynahIcons } from '@aws/mynah-ui' -import * as path from 'path' -import * as vscode from 'vscode' -import { getLogger } from '../../shared/logger/logger' -import { featureDevScheme } from '../constants' -import { - ApiClientError, - ApiServiceError, - IllegalStateTransition, - NoChangeRequiredException, - PromptRefusalException, -} from '../errors' -import { - DeletedFileInfo, - DevPhase, - Intent, - NewFileInfo, - SessionState, - SessionStateAction, - SessionStateConfig, - SessionStateInteraction, -} from '../../amazonq/commons/types' -import { registerNewFiles } from '../../amazonq/util/files' -import { randomUUID } from '../../shared/crypto' -import { collectFiles } from '../../shared/utilities/workspaceUtils' -import { i18n } from '../../shared/i18n-helper' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { FollowUpTypes } from '../../amazonq/commons/types' -import { - BaseCodeGenState, - BaseMessenger, - BasePrepareCodeGenState, - CreateNextStateParams, -} from '../../amazonq/session/sessionState' -import { LlmError } from '../../amazonq/errors' - -export class ConversationNotStartedState implements Omit { - public tokenSource: vscode.CancellationTokenSource - public readonly phase = DevPhase.INIT - - constructor(public tabID: string) { - this.tokenSource = new vscode.CancellationTokenSource() - } - - async interact(_action: SessionStateAction): Promise { - throw new IllegalStateTransition() - } -} - -export class MockCodeGenState implements SessionState { - public tokenSource: vscode.CancellationTokenSource - public filePaths: NewFileInfo[] - public deletedFiles: DeletedFileInfo[] - public readonly conversationId: string - public readonly codeGenerationId?: string - public readonly uploadId: string - - constructor( - private config: SessionStateConfig, - public tabID: string - ) { - this.tokenSource = new vscode.CancellationTokenSource() - this.filePaths = [] - this.deletedFiles = [] - this.conversationId = this.config.conversationId - this.uploadId = randomUUID() - } - - async interact(action: SessionStateAction): Promise { - // in a `mockcodegen` state, we should read from the `mock-data` folder and output - // every file retrieved in the same shape the LLM would - try { - const files = await collectFiles( - this.config.workspaceFolders.map((f) => path.join(f.uri.fsPath, './mock-data')), - this.config.workspaceFolders, - { - excludeByGitIgnore: false, - } - ) - const newFileContents = files.map((f) => ({ - zipFilePath: f.zipFilePath, - fileContent: f.fileContent, - })) - this.filePaths = registerNewFiles( - action.fs, - newFileContents, - this.uploadId, - this.config.workspaceFolders, - this.conversationId, - featureDevScheme - ) - this.deletedFiles = [ - { - zipFilePath: 'src/this-file-should-be-deleted.ts', - workspaceFolder: this.config.workspaceFolders[0], - relativePath: 'src/this-file-should-be-deleted.ts', - rejected: false, - changeApplied: false, - }, - ] - action.messenger.sendCodeResult( - this.filePaths, - this.deletedFiles, - [ - { - licenseName: 'MIT', - repository: 'foo', - url: 'foo', - }, - ], - this.tabID, - this.uploadId, - this.codeGenerationId ?? '' - ) - action.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.acceptAllChanges'), - type: FollowUpTypes.InsertCode, - icon: 'ok' as MynahIcons, - status: 'success', - }, - { - pillText: i18n('AWS.amazonq.featureDev.pillText.provideFeedback'), - type: FollowUpTypes.ProvideFeedbackAndRegenerateCode, - icon: 'refresh' as MynahIcons, - status: 'info', - }, - ], - tabID: this.tabID, - }) - } catch (e) { - // TODO: handle this error properly, double check what would be expected behaviour if mock code does not work. - getLogger().error('Unable to use mock code generation: %O', e) - } - - return { - // no point in iterating after a mocked code gen? - nextState: this, - interaction: {}, - } - } -} - -export class FeatureDevCodeGenState extends BaseCodeGenState { - protected handleProgress(messenger: Messenger, action: SessionStateAction, detail?: string): void { - if (detail) { - messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.generatingCode') + `\n\n${detail}`, - type: 'answer-part', - tabID: this.tabID, - }) - } - } - - protected getScheme(): string { - return featureDevScheme - } - - protected getTimeoutErrorCode(): string { - return 'CodeGenTimeout' - } - - protected handleGenerationComplete( - _messenger: Messenger, - _newFileInfo: NewFileInfo[], - action: SessionStateAction - ): void { - // No special handling needed for feature dev - } - - protected handleError(messenger: BaseMessenger, codegenResult: any): Error { - switch (true) { - case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { - return new ApiClientError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GetTaskAssistCodeGeneration', - 'GuardrailsException', - 400 - ) - } - case codegenResult.codeGenerationStatusDetail?.includes('PromptRefusal'): { - return new PromptRefusalException() - } - case codegenResult.codeGenerationStatusDetail?.includes('EmptyPatch'): { - if (codegenResult.codeGenerationStatusDetail?.includes('NO_CHANGE_REQUIRED')) { - return new NoChangeRequiredException() - } - return new LlmError(i18n('AWS.amazonq.featureDev.error.codeGen.default'), { - code: 'EmptyPatchException', - }) - } - case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { - return new ApiClientError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'GetTaskAssistCodeGeneration', - 'ThrottlingException', - 429 - ) - } - case codegenResult.codeGenerationStatusDetail?.includes('FileCreationFailed'): { - return new ApiServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GetTaskAssistCodeGeneration', - 'FileCreationFailedException', - 500 - ) - } - default: { - return new ApiServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GetTaskAssistCodeGeneration', - 'UnknownException', - 500 - ) - } - } - } - - protected async startCodeGeneration(action: SessionStateAction, codeGenerationId: string): Promise { - await this.config.proxyClient.startCodeGeneration( - this.config.conversationId, - this.config.uploadId, - action.msg, - Intent.DEV, - codeGenerationId, - this.currentCodeGenerationId - ) - - if (!this.isCancellationRequested) { - action.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.generatingCode'), - type: 'answer-part', - tabID: this.tabID, - }) - action.messenger.sendUpdatePlaceholder(this.tabID, i18n('AWS.amazonq.featureDev.pillText.generatingCode')) - } - } - - protected override createNextState(config: SessionStateConfig, params: CreateNextStateParams): SessionState { - return super.createNextState( - { ...config, currentCodeGenerationId: this.currentCodeGenerationId }, - params, - FeatureDevPrepareCodeGenState - ) - } -} - -export class FeatureDevPrepareCodeGenState extends BasePrepareCodeGenState { - protected preUpload(action: SessionStateAction): void { - action.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.uploadingCode'), - type: 'answer-part', - tabID: this.tabID, - }) - - action.messenger.sendUpdatePlaceholder(this.tabID, i18n('AWS.amazonq.featureDev.pillText.uploadingCode')) - } - - protected postUpload(action: SessionStateAction): void { - if (!action.tokenSource?.token.isCancellationRequested) { - action.messenger.sendAnswer({ - message: i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted'), - type: 'answer-part', - tabID: this.tabID, - }) - - action.messenger.sendUpdatePlaceholder( - this.tabID, - i18n('AWS.amazonq.featureDev.pillText.contextGatheringCompleted') - ) - } - } - - protected override createNextState(config: SessionStateConfig): SessionState { - return super.createNextState(config, FeatureDevCodeGenState) - } -} diff --git a/packages/core/src/amazonqFeatureDev/storages/chatSession.ts b/packages/core/src/amazonqFeatureDev/storages/chatSession.ts deleted file mode 100644 index f45576aa9df..00000000000 --- a/packages/core/src/amazonqFeatureDev/storages/chatSession.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { BaseChatSessionStorage } from '../../amazonq/commons/baseChatStorage' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { featureDevScheme } from '../constants' -import { Session } from '../session/session' - -export class FeatureDevChatSessionStorage extends BaseChatSessionStorage { - constructor(protected readonly messenger: Messenger) { - super() - } - - override async createSession(tabID: string): Promise { - const sessionConfig = await createSessionConfig(featureDevScheme) - const session = new Session(sessionConfig, this.messenger, tabID) - this.sessions.set(tabID, session) - return session - } -} diff --git a/packages/core/src/amazonqFeatureDev/userFacingText.ts b/packages/core/src/amazonqFeatureDev/userFacingText.ts deleted file mode 100644 index 9b8a781ef1a..00000000000 --- a/packages/core/src/amazonqFeatureDev/userFacingText.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { manageAccessGuideURL } from '../amazonq/webview/ui/texts/constants' -import { userGuideURL } from '../amazonq/webview/ui/texts/constants' -import { featureName } from './constants' - -export const examples = ` -You can use /dev to: -- Add a new feature or logic -- Write tests -- Fix a bug in your project -- Generate a README for a file, folder, or project - -To learn more, visit the _[Amazon Q Developer User Guide](${userGuideURL})_. -` - -export const uploadCodeError = `I'm sorry, I couldn't upload your workspace artifacts to Amazon S3 to help you with this task. You might need to allow access to the S3 bucket. For more information, see the [Amazon Q documentation](${manageAccessGuideURL}) or contact your network or organization administrator.` - -// Utils for logging and showing customer facing conversation id text -export const messageWithConversationId = (conversationId?: string) => - conversationId ? `\n\nConversation ID: **${conversationId}**` : '' -export const logWithConversationId = (conversationId: string) => `${featureName} Conversation ID: ${conversationId}` diff --git a/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts b/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts deleted file mode 100644 index 5d92fb7188c..00000000000 --- a/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts +++ /dev/null @@ -1,169 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ChatControllerEventEmitters } from '../../controllers/chat/controller' -import { MessageListener } from '../../../amazonq/messages/messageListener' -import { ExtensionMessage } from '../../../amazonq/webview/ui/commands' - -export interface UIMessageListenerProps { - readonly chatControllerEventEmitters: ChatControllerEventEmitters - readonly webViewMessageListener: MessageListener -} - -export class UIMessageListener { - private featureDevControllerEventsEmitters: ChatControllerEventEmitters | undefined - private webViewMessageListener: MessageListener - - constructor(props: UIMessageListenerProps) { - this.featureDevControllerEventsEmitters = props.chatControllerEventEmitters - this.webViewMessageListener = props.webViewMessageListener - - // Now we are listening to events that get sent from amazonq/webview/actions/actionListener (e.g. the tab) - this.webViewMessageListener.onMessage((msg) => { - this.handleMessage(msg) - }) - } - - private handleMessage(msg: ExtensionMessage) { - switch (msg.command) { - case 'chat-prompt': - this.processChatMessage(msg) - break - case 'follow-up-was-clicked': - this.followUpClicked(msg) - break - case 'open-diff': - this.openDiff(msg) - break - case 'chat-item-voted': - this.chatItemVoted(msg) - break - case 'chat-item-feedback': - this.chatItemFeedback(msg) - break - case 'stop-response': - this.stopResponse(msg) - break - case 'new-tab-was-created': - this.tabOpened(msg) - break - case 'tab-was-removed': - this.tabClosed(msg) - break - case 'auth-follow-up-was-clicked': - this.authClicked(msg) - break - case 'response-body-link-click': - this.processResponseBodyLinkClick(msg) - break - case 'insert_code_at_cursor_position': - this.insertCodeAtPosition(msg) - break - case 'file-click': - this.fileClicked(msg) - break - case 'store-code-result-message-id': - this.storeCodeResultMessageId(msg) - break - } - } - - private chatItemVoted(msg: any) { - this.featureDevControllerEventsEmitters?.processChatItemVotedMessage.fire({ - tabID: msg.tabID, - command: msg.command, - vote: msg.vote, - messageId: msg.messageId, - }) - } - - private chatItemFeedback(msg: any) { - this.featureDevControllerEventsEmitters?.processChatItemFeedbackMessage.fire(msg) - } - - private processChatMessage(msg: any) { - this.featureDevControllerEventsEmitters?.processHumanChatMessage.fire({ - message: msg.chatMessage, - tabID: msg.tabID, - }) - } - - private followUpClicked(msg: any) { - this.featureDevControllerEventsEmitters?.followUpClicked.fire({ - followUp: msg.followUp, - tabID: msg.tabID, - }) - } - - private fileClicked(msg: any) { - this.featureDevControllerEventsEmitters?.fileClicked.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - actionName: msg.actionName, - messageId: msg.messageId, - }) - } - - private openDiff(msg: any) { - this.featureDevControllerEventsEmitters?.openDiff.fire({ - tabID: msg.tabID, - filePath: msg.filePath, - deleted: msg.deleted, - messageId: msg.messageId, - }) - } - - private stopResponse(msg: any) { - this.featureDevControllerEventsEmitters?.stopResponse.fire({ - tabID: msg.tabID, - }) - } - - private tabOpened(msg: any) { - this.featureDevControllerEventsEmitters?.tabOpened.fire({ - tabID: msg.tabID, - }) - } - - private tabClosed(msg: any) { - this.featureDevControllerEventsEmitters?.tabClosed.fire({ - tabID: msg.tabID, - }) - } - - private authClicked(msg: any) { - this.featureDevControllerEventsEmitters?.authClicked.fire({ - tabID: msg.tabID, - authType: msg.authType, - }) - } - - private processResponseBodyLinkClick(msg: any) { - this.featureDevControllerEventsEmitters?.processResponseBodyLinkClick.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - link: msg.link, - }) - } - - private insertCodeAtPosition(msg: any) { - this.featureDevControllerEventsEmitters?.insertCodeAtPositionClicked.fire({ - command: msg.command, - messageId: msg.messageId, - tabID: msg.tabID, - code: msg.code, - insertionTargetType: msg.insertionTargetType, - codeReference: msg.codeReference, - }) - } - - private storeCodeResultMessageId(msg: any) { - this.featureDevControllerEventsEmitters?.storeCodeResultMessageId.fire({ - messageId: msg.messageId, - tabID: msg.tabID, - }) - } -} diff --git a/packages/core/src/auth/providers/sharedCredentialsProvider.ts b/packages/core/src/auth/providers/sharedCredentialsProvider.ts index 717a151a3af..407db4a717e 100644 --- a/packages/core/src/auth/providers/sharedCredentialsProvider.ts +++ b/packages/core/src/auth/providers/sharedCredentialsProvider.ts @@ -406,12 +406,28 @@ export class SharedCredentialsProvider implements CredentialsProvider { `auth: Profile ${this.profileName} is missing source_profile for role assumption` ) } - // Use source profile to assume IAM role based on role ARN provided. + + // Check if we already have resolved credentials from patchSourceCredentials const sourceProfile = iniData[profile.source_profile!] - const stsClient = new DefaultStsClient(this.getDefaultRegion() ?? 'us-east-1', { - accessKeyId: sourceProfile.aws_access_key_id!, - secretAccessKey: sourceProfile.aws_secret_access_key!, - }) + let sourceCredentials: AWS.Credentials + + if (sourceProfile.aws_access_key_id && sourceProfile.aws_secret_access_key) { + // Source credentials have already been resolved + sourceCredentials = { + accessKeyId: sourceProfile.aws_access_key_id, + secretAccessKey: sourceProfile.aws_secret_access_key, + sessionToken: sourceProfile.aws_session_token, + } + } else { + // Source profile needs credential resolution - this should have been handled by patchSourceCredentials + // but if not, we need to resolve it here + const sourceProvider = new SharedCredentialsProvider(profile.source_profile!, this.sections) + sourceCredentials = await sourceProvider.getCredentials() + } + + // Use source credentials to assume IAM role based on role ARN provided. + const stsClient = new DefaultStsClient(this.getDefaultRegion() ?? 'us-east-1', sourceCredentials) + // Prompt for MFA Token if needed. const assumeRoleReq = { RoleArn: profile.role_arn, diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 1e73b640a1e..941156a0d2e 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -49,7 +49,6 @@ import { regenerateFix, ignoreAllIssues, focusIssue, - showExploreAgentsView, showCodeIssueGroupingQuickPick, selectRegionProfileCommand, } from './commands/basicCommands' @@ -301,7 +300,6 @@ export async function activate(context: ExtContext): Promise { vscode.window.registerWebviewViewProvider(ReferenceLogViewProvider.viewType, ReferenceLogViewProvider.instance), showReferenceLog.register(), showLogs.register(), - showExploreAgentsView.register(), vscode.languages.registerCodeLensProvider( [...CodeWhispererConstants.platformLanguageIds], ReferenceInlineProvider.instance diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index a8c21b86ce2..ec1b5ae6e63 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -60,7 +60,6 @@ import { SecurityIssueProvider } from '../service/securityIssueProvider' import { CodeWhispererSettings } from '../util/codewhispererSettings' import { closeDiff, getPatchedCode } from '../../shared/utilities/diffUtils' import { insertCommentAboveLine } from '../../shared/utilities/commentUtils' -import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext' import path from 'path' import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' import { parsePatch } from 'diff' @@ -173,20 +172,6 @@ export const showLogs = Commands.declare( } ) -export const showExploreAgentsView = Commands.declare( - { id: 'aws.amazonq.exploreAgents', compositeKey: { 1: 'source' } }, - () => async (_: VsCodeCommandArg, source: CodeWhispererSource) => { - if (_ !== placeholder) { - source = 'ellipsesMenu' - } - - DefaultAmazonQAppInitContext.instance.getAppsToWebViewMessagePublisher().publish({ - sender: 'amazonqCore', - command: 'showExploreAgentsView', - }) - } -) - export const showIntroduction = Commands.declare('aws.amazonq.introduction', () => async () => { void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUriGeneral)) }) diff --git a/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts b/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts index 63c1bfe3a2f..1646864e066 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/humanInTheLoopManager.ts @@ -8,12 +8,19 @@ import path from 'path' import { FolderInfo, transformByQState } from '../../models/model' import fs from '../../../shared/fs/fs' import { createPomCopy, replacePomVersion } from './transformFileHandler' -import { IManifestFile } from '../../../amazonqFeatureDev/models' import { getLogger } from '../../../shared/logger/logger' import { telemetry } from '../../../shared/telemetry/telemetry' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { MetadataResult } from '../../../shared/telemetry/telemetryClient' +export interface IManifestFile { + pomArtifactId: string + pomFolderName: string + hilCapability: string + pomGroupId: string + sourcePomVersion: string +} + /** * @description This class helps encapsulate the "human in the loop" behavior of Amazon Q transform. Users * will be prompted for input during the transformation process. Amazon Q will make some temporary folders diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts index 88f34a799d1..2ec6fdb7c37 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts @@ -10,13 +10,13 @@ import xml2js = require('xml2js') import * as CodeWhispererConstants from '../../models/constants' import { existsSync, readFileSync, writeFileSync } from 'fs' // eslint-disable-line no-restricted-imports import { BuildSystem, DB, FolderInfo, transformByQState } from '../../models/model' -import { IManifestFile } from '../../../amazonqFeatureDev/models' import fs from '../../../shared/fs/fs' import globals from '../../../shared/extensionGlobals' import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' import { AbsolutePathDetectedError } from '../../../amazonqGumby/errors' import { getLogger } from '../../../shared/logger/logger' import AdmZip from 'adm-zip' +import { IManifestFile } from './humanInTheLoopManager' export async function getDependenciesFolderInfo(): Promise { const dependencyFolderName = `${CodeWhispererConstants.dependencyFolderName}${globals.clock.Date.now()}` diff --git a/packages/core/src/shared/constants.ts b/packages/core/src/shared/constants.ts index 48e64342e57..95d2aaac309 100644 --- a/packages/core/src/shared/constants.ts +++ b/packages/core/src/shared/constants.ts @@ -4,6 +4,7 @@ */ import * as vscode from 'vscode' +import { manageAccessGuideURL } from '../amazonq/webview/ui/texts/constants' export const profileSettingKey = 'profile' export const productName: string = 'aws-toolkit-vscode' @@ -196,3 +197,11 @@ export const amazonQVscodeMarketplace = export const crashMonitoringDirName = 'crashMonitoring' export const amazonQTabSuffix = '(Generated by Amazon Q)' + +/** + * Common strings used throughout the application + */ + +export const uploadCodeError = `I'm sorry, I couldn't upload your workspace artifacts to Amazon S3 to help you with this task. You might need to allow access to the S3 bucket. For more information, see the [Amazon Q documentation](${manageAccessGuideURL}) or contact your network or organization administrator.` + +export const featureName = 'Amazon Q Developer Agent for software development' diff --git a/packages/core/src/shared/errors.ts b/packages/core/src/shared/errors.ts index 7cec122acaf..5d114043be3 100644 --- a/packages/core/src/shared/errors.ts +++ b/packages/core/src/shared/errors.ts @@ -15,7 +15,7 @@ import type * as os from 'os' import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' import { driveLetterRegex } from './utilities/pathUtils' import { getLogger } from './logger/logger' -import { crashMonitoringDirName } from './constants' +import { crashMonitoringDirName, uploadCodeError } from './constants' import { RequestCancelledError } from './request' let _username = 'unknown-user' @@ -849,6 +849,21 @@ export class ServiceError extends ToolkitError { } } +export class UploadURLExpired extends ClientError { + constructor() { + super( + "I’m sorry, I wasn't able to generate code. A connection timed out or became unavailable. Please try again or check the following:\n\n- Exclude non-essential files in your workspace’s .gitignore.\n\n- Check that your network connection is stable.", + { code: 'UploadURLExpired' } + ) + } +} + +export class UploadCodeError extends ServiceError { + constructor(statusCode: string) { + super(uploadCodeError, { code: `UploadCode-${statusCode}` }) + } +} + export class ContentLengthError extends ClientError { constructor(message: string, info: ErrorInformation = { code: 'ContentLengthError' }) { super(message, info) diff --git a/packages/core/src/shared/utilities/diffUtils.ts b/packages/core/src/shared/utilities/diffUtils.ts index 64d09c19036..439c87dd7e6 100644 --- a/packages/core/src/shared/utilities/diffUtils.ts +++ b/packages/core/src/shared/utilities/diffUtils.ts @@ -25,7 +25,7 @@ import jaroWinkler from 'jaro-winkler' */ export async function getPatchedCode(filePath: string, patch: string, snippetMode = false) { const document = await vscode.workspace.openTextDocument(filePath) - const fileContent = document.getText() + const fileContent = document.getText().replaceAll('\r\n', '\n') // Usage with the existing getPatchedCode function: let updatedPatch = patch diff --git a/packages/core/src/shared/utilities/workspaceUtils.ts b/packages/core/src/shared/utilities/workspaceUtils.ts index 12cce75b3ff..122f2a185f4 100644 --- a/packages/core/src/shared/utilities/workspaceUtils.ts +++ b/packages/core/src/shared/utilities/workspaceUtils.ts @@ -19,7 +19,6 @@ import * as parser from '@gerhobbelt/gitignore-parser' import fs from '../fs/fs' import { ChildProcess } from './processUtils' import { isWin } from '../vscode/env' -import { maxRepoSizeBytes } from '../../amazonqFeatureDev/constants' type GitIgnoreRelativeAcceptor = { folderPath: string @@ -378,6 +377,8 @@ export async function collectFiles( const includeContent = options?.includeContent ?? true const maxFileSizeBytes = options?.maxFileSizeBytes ?? 1024 * 1024 * 10 + // Max allowed size for file collection + const maxRepoSizeBytes = 200 * 1024 * 1024 const excludeByGitIgnore = options?.excludeByGitIgnore ?? true const failOnLimit = options?.failOnLimit ?? true const inputExcludePatterns = options?.excludePatterns ?? defaultExcludePatterns diff --git a/packages/core/src/shared/vscode/env.ts b/packages/core/src/shared/vscode/env.ts index 5ee891cc7d3..abd9c58ae2d 100644 --- a/packages/core/src/shared/vscode/env.ts +++ b/packages/core/src/shared/vscode/env.ts @@ -307,6 +307,15 @@ export async function getMachineId(): Promise { // TODO: use `vscode.env.machineId` instead? return 'browser' } + // Eclipse Che-based envs (backing compute rotates, not classified as a web instance) + // TODO: use `vscode.env.machineId` instead? + if (process.env.CHE_WORKSPACE_ID) { + return process.env.CHE_WORKSPACE_ID + } + // RedHat Dev Workspaces (run some VSC web variant) + if (process.env.DEVWORKSPACE_ID) { + return process.env.DEVWORKSPACE_ID + } const proc = new ChildProcess('hostname', [], { collect: true, logging: 'no' }) // TODO: check exit code. return (await proc.run()).stdout.trim() ?? 'unknown-host' diff --git a/packages/core/src/test/amazonq/common/diff.test.ts b/packages/core/src/test/amazonq/common/diff.test.ts index 0fc81403a59..a8f3bea8747 100644 --- a/packages/core/src/test/amazonq/common/diff.test.ts +++ b/packages/core/src/test/amazonq/common/diff.test.ts @@ -13,7 +13,6 @@ import * as path from 'path' import * as vscode from 'vscode' import sinon from 'sinon' import { FileSystem } from '../../../shared/fs/fs' -import { featureDevScheme } from '../../../amazonqFeatureDev' import { createAmazonQUri, getFileDiffUris, @@ -28,6 +27,7 @@ describe('diff', () => { const filePath = path.join('/', 'foo', 'fi') const rightPath = path.join('foo', 'fee') const tabId = '0' + const featureDevScheme = 'aws-featureDev' let sandbox: sinon.SinonSandbox let executeCommandSpy: sinon.SinonSpy diff --git a/packages/core/src/test/amazonq/session/sessionState.test.ts b/packages/core/src/test/amazonq/session/sessionState.test.ts deleted file mode 100644 index dcff3398cea..00000000000 --- a/packages/core/src/test/amazonq/session/sessionState.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import sinon from 'sinon' -import { CodeGenBase } from '../../../amazonq/session/sessionState' -import { RunCommandLogFileName } from '../../../amazonq/session/sessionState' -import assert from 'assert' -import * as workspaceUtils from '../../../shared/utilities/workspaceUtils' -import { TelemetryHelper } from '../../../amazonq/util/telemetryHelper' -import { assertLogsContain } from '../../globalSetup.test' - -describe('CodeGenBase generateCode log file handling', () => { - class TestCodeGen extends CodeGenBase { - public generatedFiles: any[] = [] - constructor(config: any, tabID: string) { - super(config, tabID) - } - protected handleProgress(_messenger: any): void { - // No-op for test. - } - protected getScheme(): string { - return 'file' - } - protected getTimeoutErrorCode(): string { - return 'test_timeout' - } - protected handleGenerationComplete(_messenger: any, newFileInfo: any[]): void { - this.generatedFiles = newFileInfo - } - protected handleError(_messenger: any, _codegenResult: any): Error { - throw new Error('handleError called') - } - } - - let fakeProxyClient: any - let testConfig: any - let fsMock: any - let messengerMock: any - let testAction: any - - beforeEach(async () => { - const ret = { - testworkspacefolder: { - uri: vscode.Uri.file('/path/to/testworkspacefolder'), - name: 'testworkspacefolder', - index: 0, - }, - } - sinon.stub(workspaceUtils, 'getWorkspaceFoldersByPrefixes').returns(ret) - - fakeProxyClient = { - getCodeGeneration: sinon.stub().resolves({ - codeGenerationStatus: { status: 'Complete' }, - codeGenerationRemainingIterationCount: 0, - codeGenerationTotalIterationCount: 1, - }), - exportResultArchive: sinon.stub(), - } - - testConfig = { - conversationId: 'conv_test', - uploadId: 'upload_test', - workspaceRoots: ['/path/to/testworkspacefolder'], - proxyClient: fakeProxyClient, - } - - fsMock = { - writeFile: sinon.stub().resolves(), - registerProvider: sinon.stub().resolves(), - } - - messengerMock = { sendAnswer: sinon.spy() } - - testAction = { - fs: fsMock, - messenger: messengerMock, - tokenSource: { - token: { - isCancellationRequested: false, - onCancellationRequested: () => {}, - }, - }, - } - }) - - afterEach(() => { - sinon.restore() - }) - - const runGenerateCode = async (codeGenerationId: string) => { - const testCodeGen = new TestCodeGen(testConfig, 'tab1') - return await testCodeGen.generateCode({ - messenger: messengerMock, - fs: fsMock, - codeGenerationId, - telemetry: new TelemetryHelper(), - workspaceFolders: [testConfig.workspaceRoots[0]], - action: testAction, - }) - } - - const createExpectedNewFile = (fileObj: { zipFilePath: string; fileContent: string }) => ({ - zipFilePath: fileObj.zipFilePath, - fileContent: fileObj.fileContent, - changeApplied: false, - rejected: false, - relativePath: fileObj.zipFilePath, - virtualMemoryUri: vscode.Uri.file(`/upload_test/${fileObj.zipFilePath}`), - workspaceFolder: { - index: 0, - name: 'testworkspacefolder', - uri: vscode.Uri.file('/path/to/testworkspacefolder'), - }, - }) - - it('adds the log content to logger if present and excludes it from new files', async () => { - const logFileInfo = { - zipFilePath: RunCommandLogFileName, - fileContent: 'Log content', - } - const otherFile = { zipFilePath: 'other.ts', fileContent: 'other content' } - fakeProxyClient.exportResultArchive.resolves({ - newFileContents: [logFileInfo, otherFile], - deletedFiles: [], - references: [], - }) - const result = await runGenerateCode('codegen1') - - assertLogsContain(`sessionState: Run Command logs, Log content`, false, 'info') - - const expectedNewFile = createExpectedNewFile(otherFile) - assert.deepStrictEqual(result.newFiles[0].fileContent, expectedNewFile.fileContent) - }) - - it('skips log file handling if log file is not present', async () => { - const file1 = { zipFilePath: 'file1.ts', fileContent: 'content1' } - fakeProxyClient.exportResultArchive.resolves({ - newFileContents: [file1], - deletedFiles: [], - references: [], - }) - - const result = await runGenerateCode('codegen2') - - assert.throws(() => assertLogsContain(`sessionState: Run Command logs, Log content`, false, 'info')) - - const expectedNewFile = createExpectedNewFile(file1) - assert.deepStrictEqual(result.newFiles[0].fileContent, expectedNewFile.fileContent) - }) -}) diff --git a/packages/core/src/test/amazonq/session/testSetup.ts b/packages/core/src/test/amazonq/session/testSetup.ts deleted file mode 100644 index 76f2c90f94f..00000000000 --- a/packages/core/src/test/amazonq/session/testSetup.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import sinon from 'sinon' -import { createBasicTestConfig, createMockSessionStateConfig, TestSessionMocks } from '../utils' -import { SessionStateConfig } from '../../../amazonq' - -export function createSessionTestSetup() { - const conversationId = 'conversation-id' - const uploadId = 'upload-id' - const tabId = 'tab-id' - const currentCodeGenerationId = '' - - return { - conversationId, - uploadId, - tabId, - currentCodeGenerationId, - } -} - -export async function createTestConfig( - testMocks: TestSessionMocks, - conversationId: string, - uploadId: string, - currentCodeGenerationId: string -) { - testMocks.getCodeGeneration = sinon.stub() - testMocks.exportResultArchive = sinon.stub() - testMocks.createUploadUrl = sinon.stub() - const basicConfig = await createBasicTestConfig(conversationId, uploadId, currentCodeGenerationId) - const testConfig = createMockSessionStateConfig(basicConfig, testMocks) - return testConfig -} - -export interface TestContext { - conversationId: string - uploadId: string - tabId: string - currentCodeGenerationId: string - testConfig: SessionStateConfig - testMocks: Record -} - -export function createTestContext(): TestContext { - const { conversationId, uploadId, tabId, currentCodeGenerationId } = createSessionTestSetup() - - return { - conversationId, - uploadId, - tabId, - currentCodeGenerationId, - testConfig: {} as SessionStateConfig, - testMocks: {}, - } -} - -export function setupTestHooks(context: TestContext) { - beforeEach(async () => { - context.testMocks = {} - context.testConfig = await createTestConfig( - context.testMocks, - context.conversationId, - context.uploadId, - context.currentCodeGenerationId - ) - }) - - afterEach(() => { - sinon.restore() - }) -} diff --git a/packages/core/src/test/amazonq/utils.ts b/packages/core/src/test/amazonq/utils.ts deleted file mode 100644 index ec2e7020e4e..00000000000 --- a/packages/core/src/test/amazonq/utils.ts +++ /dev/null @@ -1,182 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { MessagePublisher } from '../../amazonq/messages/messagePublisher' -import { ChatControllerEventEmitters, FeatureDevController } from '../../amazonqFeatureDev/controllers/chat/controller' -import { FeatureDevChatSessionStorage } from '../../amazonqFeatureDev/storages/chatSession' -import { createTestWorkspaceFolder } from '../testUtil' -import { Session } from '../../amazonqFeatureDev/session/session' -import { SessionState, SessionStateAction, SessionStateConfig } from '../../amazonq/commons/types' -import { FeatureDevClient } from '../../amazonqFeatureDev/client/featureDev' -import { VirtualMemoryFile } from '../../shared/virtualMemoryFile' -import path from 'path' -import { featureDevChat } from '../../amazonqFeatureDev/constants' -import { Messenger } from '../../amazonq/commons/connector/baseMessenger' -import { AppToWebViewMessageDispatcher } from '../../amazonq/commons/connector/connectorMessages' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { VirtualFileSystem } from '../../shared' -import { TelemetryHelper } from '../../amazonq/util/telemetryHelper' -import { FeatureClient } from '../../amazonq/client/client' - -export function createMessenger(): Messenger { - return new Messenger( - new AppToWebViewMessageDispatcher(new MessagePublisher(sinon.createStubInstance(vscode.EventEmitter))), - featureDevChat - ) -} - -export function createMockChatEmitters(): ChatControllerEventEmitters { - return { - processHumanChatMessage: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - stopResponse: new vscode.EventEmitter(), - tabOpened: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - insertCodeAtPositionClicked: new vscode.EventEmitter(), - fileClicked: new vscode.EventEmitter(), - storeCodeResultMessageId: new vscode.EventEmitter(), - } -} - -export interface ControllerSetup { - emitters: ChatControllerEventEmitters - workspaceFolder: vscode.WorkspaceFolder - messenger: Messenger - sessionStorage: FeatureDevChatSessionStorage -} - -export async function createSession({ - messenger, - sessionState, - scheme, - conversationID = '0', - tabID = '0', - uploadID = '0', -}: { - messenger: Messenger - scheme: string - sessionState?: Omit - conversationID?: string - tabID?: string - uploadID?: string -}) { - const sessionConfig = await createSessionConfig(scheme) - - const client = sinon.createStubInstance(FeatureDevClient) - client.createConversation.resolves(conversationID) - const session = new Session(sessionConfig, messenger, tabID, sessionState, client) - - sinon.stub(session, 'conversationId').get(() => conversationID) - sinon.stub(session, 'uploadId').get(() => uploadID) - - return session -} - -export async function sessionRegisterProvider(session: Session, uri: vscode.Uri, fileContents: Uint8Array) { - session.config.fs.registerProvider(uri, new VirtualMemoryFile(fileContents)) -} - -export function generateVirtualMemoryUri(uploadID: string, filePath: string, scheme: string) { - const generationFilePath = path.join(uploadID, filePath) - const uri = vscode.Uri.from({ scheme, path: generationFilePath }) - return uri -} - -export async function sessionWriteFile(session: Session, uri: vscode.Uri, encodedContent: Uint8Array) { - await session.config.fs.writeFile(uri, encodedContent, { - create: true, - overwrite: true, - }) -} - -export async function createController(): Promise { - const messenger = createMessenger() - - // Create a new workspace root - const testWorkspaceFolder = await createTestWorkspaceFolder() - sinon.stub(vscode.workspace, 'workspaceFolders').value([testWorkspaceFolder]) - - const sessionStorage = new FeatureDevChatSessionStorage(messenger) - - const mockChatControllerEventEmitters = createMockChatEmitters() - - new FeatureDevController( - mockChatControllerEventEmitters, - messenger, - sessionStorage, - sinon.createStubInstance(vscode.EventEmitter).event - ) - - return { - emitters: mockChatControllerEventEmitters, - workspaceFolder: testWorkspaceFolder, - messenger, - sessionStorage, - } -} - -export function createMockSessionStateAction(msg?: string): SessionStateAction { - return { - task: 'test-task', - msg: msg ?? 'test-msg', - fs: new VirtualFileSystem(), - messenger: new Messenger( - new AppToWebViewMessageDispatcher(new MessagePublisher(new vscode.EventEmitter())), - featureDevChat - ), - telemetry: new TelemetryHelper(), - uploadHistory: {}, - } -} - -export interface TestSessionMocks { - getCodeGeneration?: sinon.SinonStub - exportResultArchive?: sinon.SinonStub - createUploadUrl?: sinon.SinonStub -} - -export interface SessionTestConfig { - conversationId: string - uploadId: string - workspaceFolder: vscode.WorkspaceFolder - currentCodeGenerationId?: string -} - -export function createMockSessionStateConfig(config: SessionTestConfig, mocks: TestSessionMocks): SessionStateConfig { - return { - workspaceRoots: ['fake-source'], - workspaceFolders: [config.workspaceFolder], - conversationId: config.conversationId, - proxyClient: { - createConversation: () => sinon.stub(), - createUploadUrl: () => mocks.createUploadUrl!(), - startCodeGeneration: () => sinon.stub(), - getCodeGeneration: () => mocks.getCodeGeneration!(), - exportResultArchive: () => mocks.exportResultArchive!(), - } as unknown as FeatureClient, - uploadId: config.uploadId, - currentCodeGenerationId: config.currentCodeGenerationId, - } -} - -export async function createBasicTestConfig( - conversationId: string = 'conversation-id', - uploadId: string = 'upload-id', - currentCodeGenerationId: string = '' -): Promise { - return { - conversationId, - uploadId, - workspaceFolder: await createTestWorkspaceFolder('fake-root'), - currentCodeGenerationId, - } -} diff --git a/packages/core/src/test/amazonqDoc/controller.test.ts b/packages/core/src/test/amazonqDoc/controller.test.ts deleted file mode 100644 index d69edc47fd7..00000000000 --- a/packages/core/src/test/amazonqDoc/controller.test.ts +++ /dev/null @@ -1,577 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' -import sinon from 'sinon' -import { - assertTelemetry, - ControllerSetup, - createController, - createExpectedEvent, - createExpectedMetricData, - createSession, - EventMetrics, - FollowUpSequences, - generateVirtualMemoryUri, - updateFilePaths, -} from './utils' -import { CurrentWsFolders, MetricDataOperationName, MetricDataResult, NewFileInfo } from '../../amazonqDoc/types' -import { DocCodeGenState, docScheme, Session } from '../../amazonqDoc' -import { AuthUtil } from '../../codewhisperer' -import { - ApiClientError, - ApiServiceError, - CodeIterationLimitError, - FeatureDevClient, - getMetricResult, - MonthlyConversationLimitError, - PrepareRepoFailedError, - TabIdNotFoundError, - UploadCodeError, - UploadURLExpired, - UserMessageNotFoundError, - ZipFileError, -} from '../../amazonqFeatureDev' -import { i18n, ToolkitError, waitUntil } from '../../shared' -import { FollowUpTypes } from '../../amazonq/commons/types' -import { FileSystem } from '../../shared/fs/fs' -import { ReadmeBuilder } from './mockContent' -import * as path from 'path' -import { - ContentLengthError, - NoChangeRequiredException, - PromptRefusalException, - PromptTooVagueError, - PromptUnrelatedError, - ReadmeTooLargeError, - ReadmeUpdateTooLargeError, - WorkspaceEmptyError, -} from '../../amazonqDoc/errors' -import { LlmError } from '../../amazonq/errors' -describe('Controller - Doc Generation', () => { - const firstTabID = '123' - const firstConversationID = '123' - const firstUploadID = '123' - - const secondTabID = '456' - const secondConversationID = '456' - const secondUploadID = '456' - - let controllerSetup: ControllerSetup - let session: Session - let sendDocTelemetrySpy: sinon.SinonStub - let sendDocTelemetrySpyForSecondTab: sinon.SinonStub - let mockGetCodeGeneration: sinon.SinonStub - let getSessionStub: sinon.SinonStub - let modifiedReadme: string - const generatedReadme = ReadmeBuilder.createBaseReadme() - let sandbox: sinon.SinonSandbox - - const getFilePaths = (controllerSetup: ControllerSetup, uploadID: string): NewFileInfo[] => [ - { - zipFilePath: path.normalize('README.md'), - relativePath: path.normalize('README.md'), - fileContent: generatedReadme, - rejected: false, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, path.normalize('README.md'), docScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ] - - async function createCodeGenState( - sandbox: sinon.SinonSandbox, - tabID: string, - conversationID: string, - uploadID: string - ) { - mockGetCodeGeneration = sandbox.stub().resolves({ codeGenerationStatus: { status: 'Complete' } }) - - const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders - const testConfig = { - conversationId: conversationID, - proxyClient: { - createConversation: () => sandbox.stub(), - createUploadUrl: () => sandbox.stub(), - generatePlan: () => sandbox.stub(), - startCodeGeneration: () => sandbox.stub(), - getCodeGeneration: () => mockGetCodeGeneration(), - exportResultArchive: () => sandbox.stub(), - } as unknown as FeatureDevClient, - workspaceRoots: [''], - uploadId: uploadID, - workspaceFolders, - } - - const codeGenState = new DocCodeGenState( - testConfig, - getFilePaths(controllerSetup, uploadID), - [], - [], - tabID, - 0, - {} - ) - return createSession({ - messenger: controllerSetup.messenger, - sessionState: codeGenState, - conversationID, - tabID, - uploadID, - scheme: docScheme, - sandbox, - }) - } - async function fireFollowUps(followUpTypes: FollowUpTypes[], stub: sinon.SinonStub, tabID: string) { - for (const type of followUpTypes) { - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { type }, - }) - await waitForStub(stub) - } - } - - async function waitForStub(stub: sinon.SinonStub) { - await waitUntil(() => Promise.resolve(stub.callCount > 0), {}) - } - - async function performAction( - action: 'generate' | 'update' | 'makeChanges' | 'accept' | 'edit', - getSessionStub: sinon.SinonStub, - message?: string, - tabID = firstTabID, - conversationID = firstConversationID - ) { - const sequences = { - generate: FollowUpSequences.generateReadme, - update: FollowUpSequences.updateReadme, - edit: FollowUpSequences.editReadme, - makeChanges: FollowUpSequences.makeChanges, - accept: FollowUpSequences.acceptContent, - } - - await fireFollowUps(sequences[action], getSessionStub, tabID) - - if ((action === 'makeChanges' || action === 'edit') && message) { - controllerSetup.emitters.processHumanChatMessage.fire({ - tabID, - conversationID, - message, - }) - await waitForStub(getSessionStub) - } - } - - async function setupTest(sandbox: sinon.SinonSandbox, isMultiTabs?: boolean, error?: ToolkitError) { - controllerSetup = await createController(sandbox) - session = await createCodeGenState(sandbox, firstTabID, firstConversationID, firstUploadID) - sendDocTelemetrySpy = sandbox.stub(session, 'sendDocTelemetryEvent').resolves() - sandbox.stub(session, 'preloader').resolves() - error ? sandbox.stub(session, 'send').throws(error) : sandbox.stub(session, 'send').resolves() - Object.defineProperty(session, '_conversationId', { - value: firstConversationID, - writable: true, - configurable: true, - }) - - sandbox.stub(AuthUtil.instance, 'getChatAuthState').resolves({ - codewhispererCore: 'connected', - codewhispererChat: 'connected', - amazonQ: 'connected', - }) - sandbox.stub(FileSystem.prototype, 'exists').resolves(false) - if (isMultiTabs) { - const secondSession = await createCodeGenState(sandbox, secondTabID, secondConversationID, secondUploadID) - sendDocTelemetrySpyForSecondTab = sandbox.stub(secondSession, 'sendDocTelemetryEvent').resolves() - sandbox.stub(secondSession, 'preloader').resolves() - sandbox.stub(secondSession, 'send').resolves() - Object.defineProperty(secondSession, '_conversationId', { - value: secondConversationID, - writable: true, - configurable: true, - }) - getSessionStub = sandbox - .stub(controllerSetup.sessionStorage, 'getSession') - .callsFake(async (tabId: string): Promise => { - if (tabId === firstTabID) { - return session - } - if (tabId === secondTabID) { - return secondSession - } - throw new Error(`Unknown tab ID: ${tabId}`) - }) - } else { - getSessionStub = sandbox.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - } - modifiedReadme = ReadmeBuilder.createReadmeWithRepoStructure() - sandbox - .stub(vscode.workspace, 'openTextDocument') - .callsFake(async (options?: string | vscode.Uri | { language?: string; content?: string }) => { - let documentPath = '' - if (typeof options === 'string') { - documentPath = options - } else if (options && 'path' in options) { - documentPath = options.path - } - - const isTempFile = documentPath === 'empty' - return { - getText: () => (isTempFile ? generatedReadme : modifiedReadme), - } as any - }) - } - - const retryTest = async ( - testMethod: () => Promise, - isMultiTabs?: boolean, - error?: ToolkitError, - maxRetries: number = 3, - delayMs: number = 1000 - ): Promise => { - let lastError: Error | undefined - - for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { - sandbox = sinon.createSandbox() - sandbox.useFakeTimers({ - now: new Date('2025-03-20T12:00:00.000Z'), - toFake: ['Date'], - }) - try { - await setupTest(sandbox, isMultiTabs, error) - await testMethod() - sandbox.restore() - return - } catch (error) { - lastError = error as Error - sandbox.restore() - - if (attempt > maxRetries) { - console.error(`Test failed after ${maxRetries} retries:`, lastError) - throw lastError - } - - console.log(`Test attempt ${attempt} failed, retrying...`) - await new Promise((resolve) => setTimeout(resolve, delayMs)) - } - } - } - - after(() => { - if (sandbox) { - sandbox.restore() - } - }) - - it('should emit generation telemetry for initial README generation', async () => { - await retryTest(async () => { - await performAction('generate', getSessionStub) - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - it('should emit another generation telemetry for make changes operation after initial README generation', async () => { - await retryTest(async () => { - await performAction('generate', getSessionStub) - const firstExpectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: firstExpectedEvent, - type: 'generation', - sandbox, - }) - - await updateFilePaths(session, modifiedReadme, firstUploadID, docScheme, controllerSetup.workspaceFolder) - await performAction('makeChanges', getSessionStub, 'add repository structure section') - - const secondExpectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: secondExpectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - - it('should emit acceptance telemetry for README generation', async () => { - await retryTest(async () => { - await performAction('generate', getSessionStub) - await new Promise((resolve) => setTimeout(resolve, 100)) - const expectedEvent = createExpectedEvent({ - type: 'acceptance', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await performAction('accept', getSessionStub) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'acceptance', - sandbox, - }) - }) - }) - it('should emit generation telemetry for README update', async () => { - await retryTest(async () => { - await performAction('update', getSessionStub) - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'UPDATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - it('should emit another generation telemetry for make changes operation after README update', async () => { - await retryTest(async () => { - await performAction('update', getSessionStub) - await new Promise((resolve) => setTimeout(resolve, 100)) - - modifiedReadme = ReadmeBuilder.createReadmeWithDataFlow() - await updateFilePaths(session, modifiedReadme, firstUploadID, docScheme, controllerSetup.workspaceFolder) - - await performAction('makeChanges', getSessionStub, 'add data flow section') - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.DATA_FLOW, - interactionType: 'UPDATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - - it('should emit acceptance telemetry for README update', async () => { - await retryTest(async () => { - await performAction('update', getSessionStub) - await new Promise((resolve) => setTimeout(resolve, 100)) - - const expectedEvent = createExpectedEvent({ - type: 'acceptance', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'UPDATE_README', - conversationId: firstConversationID, - }) - - await performAction('accept', getSessionStub) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'acceptance', - sandbox, - }) - }) - }) - - it('should emit generation telemetry for README edit', async () => { - await retryTest(async () => { - await performAction('edit', getSessionStub, 'add repository structure section') - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'EDIT_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - }) - }) - it('should emit acceptance telemetry for README edit', async () => { - await retryTest(async () => { - await performAction('edit', getSessionStub, 'add repository structure section') - await new Promise((resolve) => setTimeout(resolve, 100)) - - const expectedEvent = createExpectedEvent({ - type: 'acceptance', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'EDIT_README', - conversationId: firstConversationID, - }) - - await performAction('accept', getSessionStub) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'acceptance', - sandbox, - }) - }) - }) - it('should emit separate telemetry events when executing /doc in different tabs', async () => { - await retryTest(async () => { - const firstSession = await getSessionStub(firstTabID) - const secondSession = await getSessionStub(secondTabID) - await performAction('generate', firstSession) - await performAction('update', secondSession, undefined, secondTabID, secondConversationID) - - const expectedEvent = createExpectedEvent({ - type: 'generation', - ...EventMetrics.INITIAL_README, - interactionType: 'GENERATE_README', - conversationId: firstConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent, - type: 'generation', - sandbox, - }) - - const expectedEventForSecondTab = createExpectedEvent({ - type: 'generation', - ...EventMetrics.REPO_STRUCTURE, - interactionType: 'UPDATE_README', - conversationId: secondConversationID, - }) - - await assertTelemetry({ - spy: sendDocTelemetrySpyForSecondTab, - expectedEvent: expectedEventForSecondTab, - type: 'generation', - sandbox, - }) - }, true) - }) - - describe('Doc Generation Error Handling', () => { - const errors = [ - { - name: 'MonthlyConversationLimitError', - error: new MonthlyConversationLimitError('Service Quota Exceeded'), - }, - { - name: 'DocGenerationGuardrailsException', - error: new ApiClientError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'GuardrailsException', - 400 - ), - }, - { - name: 'DocGenerationEmptyPatchException', - error: new LlmError(i18n('AWS.amazonq.doc.error.docGen.default'), { - code: 'EmptyPatchException', - }), - }, - { - name: 'DocGenerationThrottlingException', - error: new ApiClientError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'GetTaskAssistCodeGeneration', - 'ThrottlingException', - 429 - ), - }, - { name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') }, - { name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() }, - { name: 'TabIdNotFoundError', error: new TabIdNotFoundError() }, - { name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() }, - { name: 'PromptRefusalException', error: new PromptRefusalException(0) }, - { name: 'ZipFileError', error: new ZipFileError() }, - { name: 'CodeIterationLimitError', error: new CodeIterationLimitError() }, - { name: 'UploadURLExpired', error: new UploadURLExpired() }, - { name: 'NoChangeRequiredException', error: new NoChangeRequiredException() }, - { name: 'ReadmeTooLargeError', error: new ReadmeTooLargeError() }, - { name: 'ReadmeUpdateTooLargeError', error: new ReadmeUpdateTooLargeError(0) }, - { name: 'ContentLengthError', error: new ContentLengthError() }, - { name: 'WorkspaceEmptyError', error: new WorkspaceEmptyError() }, - { name: 'PromptUnrelatedError', error: new PromptUnrelatedError(0) }, - { name: 'PromptTooVagueError', error: new PromptTooVagueError(0) }, - { name: 'PromptRefusalException', error: new PromptRefusalException(0) }, - { - name: 'default', - error: new ApiServiceError( - i18n('AWS.amazonq.doc.error.docGen.default'), - 'GetTaskAssistCodeGeneration', - 'UnknownException', - 500 - ), - }, - ] - for (const { name, error } of errors) { - it(`should emit failure operation telemetry when ${name} occurs`, async () => { - await retryTest( - async () => { - await performAction('generate', getSessionStub) - - const expectedSuccessMetric = createExpectedMetricData( - MetricDataOperationName.StartDocGeneration, - MetricDataResult.Success - ) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: expectedSuccessMetric, - type: 'metric', - sandbox, - }) - - const expectedFailureMetric = createExpectedMetricData( - MetricDataOperationName.EndDocGeneration, - getMetricResult(error) - ) - await assertTelemetry({ - spy: sendDocTelemetrySpy, - expectedEvent: expectedFailureMetric, - type: 'metric', - sandbox, - }) - }, - undefined, - error - ) - }) - } - }) -}) diff --git a/packages/core/src/test/amazonqDoc/mockContent.ts b/packages/core/src/test/amazonqDoc/mockContent.ts deleted file mode 100644 index 1f3e68f6a58..00000000000 --- a/packages/core/src/test/amazonqDoc/mockContent.ts +++ /dev/null @@ -1,86 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -export const ReadmeSections = { - HEADER: `# My Awesome Project - -This is a demo project showcasing various features and capabilities.`, - - GETTING_STARTED: `## Getting Started -1. Clone the repository -2. Run npm install -3. Start the application`, - - FEATURES: `## Features -- Fast processing -- Easy to use -- Well documented`, - - LICENSE: '## License\nMIT License', - - REPO_STRUCTURE: `## Repository Structure -/src - /components - /utils -/tests - /unit -/docs`, - - DATA_FLOW: `## Data Flow -1. Input processing - - Data validation - - Format conversion -2. Core processing - - Business logic - - Data transformation -3. Output generation - - Result formatting - - Response delivery`, -} as const - -export class ReadmeBuilder { - private sections: string[] = [] - - addSection(section: string): this { - this.sections.push(section.replace(/\r\n/g, '\n')) - return this - } - - build(): string { - return this.sections.join('\n\n').replace(/\r\n/g, '\n') - } - - static createBaseReadme(): string { - return new ReadmeBuilder() - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.HEADER)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.GETTING_STARTED)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.FEATURES)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.LICENSE)) - .build() - } - - static createReadmeWithRepoStructure(): string { - return new ReadmeBuilder() - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.HEADER)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.REPO_STRUCTURE)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.GETTING_STARTED)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.FEATURES)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.LICENSE)) - .build() - } - - static createReadmeWithDataFlow(): string { - return new ReadmeBuilder() - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.HEADER)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.GETTING_STARTED)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.FEATURES)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.DATA_FLOW)) - .addSection(ReadmeBuilder.normalizeSection(ReadmeSections.LICENSE)) - .build() - } - - private static normalizeSection(section: string): string { - return section.replace(/\r\n/g, '\n') - } -} diff --git a/packages/core/src/test/amazonqDoc/session/sessionState.test.ts b/packages/core/src/test/amazonqDoc/session/sessionState.test.ts deleted file mode 100644 index 8f96894cc22..00000000000 --- a/packages/core/src/test/amazonqDoc/session/sessionState.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import assert from 'assert' -import sinon from 'sinon' -import { DocPrepareCodeGenState } from '../../../amazonqDoc' -import { createMockSessionStateAction } from '../../amazonq/utils' - -import { createTestContext, setupTestHooks } from '../../amazonq/session/testSetup' - -describe('sessionStateDoc', () => { - const context = createTestContext() - setupTestHooks(context) - - describe('DocPrepareCodeGenState', () => { - it('error when failing to prepare repo information', async () => { - sinon.stub(vscode.workspace, 'findFiles').throws() - context.testMocks.createUploadUrl!.resolves({ uploadId: '', uploadUrl: '' }) - const testAction = createMockSessionStateAction() - - await assert.rejects(() => { - return new DocPrepareCodeGenState(context.testConfig, [], [], [], context.tabId, 0).interact(testAction) - }) - }) - }) -}) diff --git a/packages/core/src/test/amazonqDoc/utils.ts b/packages/core/src/test/amazonqDoc/utils.ts deleted file mode 100644 index 51c7305902c..00000000000 --- a/packages/core/src/test/amazonqDoc/utils.ts +++ /dev/null @@ -1,269 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as sinon from 'sinon' -import { MessagePublisher } from '../../amazonq/messages/messagePublisher' -import { ChatControllerEventEmitters, DocController } from '../../amazonqDoc/controllers/chat/controller' -import { DocChatSessionStorage } from '../../amazonqDoc/storages/chatSession' -import { createTestWorkspaceFolder } from '../testUtil' -import { Session } from '../../amazonqDoc/session/session' -import { NewFileInfo, SessionState } from '../../amazonqDoc/types' -import { FeatureDevClient } from '../../amazonqFeatureDev/client/featureDev' -import { VirtualMemoryFile } from '../../shared/virtualMemoryFile' -import path from 'path' -import { docChat } from '../../amazonqDoc/constants' -import { DocMessenger } from '../../amazonqDoc/messenger' -import { AppToWebViewMessageDispatcher } from '../../amazonq/commons/connector/connectorMessages' -import { createSessionConfig } from '../../amazonq/commons/session/sessionConfigFactory' -import { - DocV2GenerationEvent, - DocV2AcceptanceEvent, - MetricData, -} from '../../amazonqFeatureDev/client/featuredevproxyclient' -import { FollowUpTypes } from '../../amazonq/commons/types' - -export function createMessenger(sandbox: sinon.SinonSandbox): DocMessenger { - return new DocMessenger( - new AppToWebViewMessageDispatcher(new MessagePublisher(sandbox.createStubInstance(vscode.EventEmitter))), - docChat - ) -} - -export function createMockChatEmitters(): ChatControllerEventEmitters { - return { - processHumanChatMessage: new vscode.EventEmitter(), - followUpClicked: new vscode.EventEmitter(), - openDiff: new vscode.EventEmitter(), - processChatItemVotedMessage: new vscode.EventEmitter(), - processChatItemFeedbackMessage: new vscode.EventEmitter(), - stopResponse: new vscode.EventEmitter(), - tabOpened: new vscode.EventEmitter(), - tabClosed: new vscode.EventEmitter(), - authClicked: new vscode.EventEmitter(), - processResponseBodyLinkClick: new vscode.EventEmitter(), - insertCodeAtPositionClicked: new vscode.EventEmitter(), - fileClicked: new vscode.EventEmitter(), - formActionClicked: new vscode.EventEmitter(), - } -} - -export interface ControllerSetup { - emitters: ChatControllerEventEmitters - workspaceFolder: vscode.WorkspaceFolder - messenger: DocMessenger - sessionStorage: DocChatSessionStorage -} - -export async function createSession({ - messenger, - sessionState, - scheme, - conversationID = '0', - tabID = '0', - uploadID = '0', - sandbox, -}: { - messenger: DocMessenger - scheme: string - sessionState?: Omit - conversationID?: string - tabID?: string - uploadID?: string - sandbox: sinon.SinonSandbox -}) { - const sessionConfig = await createSessionConfig(scheme) - - const client = sandbox.createStubInstance(FeatureDevClient) - client.createConversation.resolves(conversationID) - const session = new Session(sessionConfig, messenger, tabID, sessionState, client) - - sandbox.stub(session, 'conversationId').get(() => conversationID) - sandbox.stub(session, 'uploadId').get(() => uploadID) - - return session -} -export async function sessionRegisterProvider(session: Session, uri: vscode.Uri, fileContents: Uint8Array) { - session.config.fs.registerProvider(uri, new VirtualMemoryFile(fileContents)) -} - -export function generateVirtualMemoryUri(uploadID: string, filePath: string, scheme: string) { - const generationFilePath = path.join(uploadID, filePath) - const uri = vscode.Uri.from({ scheme, path: generationFilePath }) - return uri -} - -export async function sessionWriteFile(session: Session, uri: vscode.Uri, encodedContent: Uint8Array) { - await session.config.fs.writeFile(uri, encodedContent, { - create: true, - overwrite: true, - }) -} - -export async function createController(sandbox: sinon.SinonSandbox): Promise { - const messenger = createMessenger(sandbox) - - // Create a new workspace root - const testWorkspaceFolder = await createTestWorkspaceFolder() - sandbox.stub(vscode.workspace, 'workspaceFolders').value([testWorkspaceFolder]) - - const sessionStorage = new DocChatSessionStorage(messenger) - - const mockChatControllerEventEmitters = createMockChatEmitters() - - new DocController( - mockChatControllerEventEmitters, - messenger, - sessionStorage, - sandbox.createStubInstance(vscode.EventEmitter).event - ) - - return { - emitters: mockChatControllerEventEmitters, - workspaceFolder: testWorkspaceFolder, - messenger, - sessionStorage, - } -} - -export type EventParams = { - type: 'generation' | 'acceptance' - chars: number - lines: number - files: number - interactionType: 'GENERATE_README' | 'UPDATE_README' | 'EDIT_README' - callIndex?: number - conversationId: string -} -/** - * Metrics for measuring README content changes in documentation generation tests. - */ -export const EventMetrics = { - /** - * Initial README content measurements - * Generated using ReadmeBuilder.createBaseReadme() - */ - INITIAL_README: { - chars: 265, - lines: 16, - files: 1, - }, - /** - * Repository Structure section measurements - * Differential metrics when adding repository structure documentation compare to the initial readme - */ - REPO_STRUCTURE: { - chars: 60, - lines: 8, - files: 1, - }, - /** - * Data Flow section measurements - * Differential metrics when adding data flow documentation compare to the initial readme - */ - DATA_FLOW: { - chars: 180, - lines: 11, - files: 1, - }, -} as const - -export function createExpectedEvent(params: EventParams) { - const baseEvent = { - conversationId: params.conversationId, - numberOfNavigations: 1, - folderLevel: 'ENTIRE_WORKSPACE', - interactionType: params.interactionType, - } - - if (params.type === 'generation') { - return { - ...baseEvent, - numberOfGeneratedChars: params.chars, - numberOfGeneratedLines: params.lines, - numberOfGeneratedFiles: params.files, - } as DocV2GenerationEvent - } else { - return { - ...baseEvent, - numberOfAddedChars: params.chars, - numberOfAddedLines: params.lines, - numberOfAddedFiles: params.files, - userDecision: 'ACCEPT', - } as DocV2AcceptanceEvent - } -} - -export function createExpectedMetricData(operationName: string, result: string) { - return { - metricName: 'Operation', - metricValue: 1, - timestamp: new Date(), - product: 'DocGeneration', - dimensions: [ - { - name: 'operationName', - value: operationName, - }, - { - name: 'result', - value: result, - }, - ], - } -} - -export async function assertTelemetry(params: { - spy: sinon.SinonStub - expectedEvent: DocV2GenerationEvent | DocV2AcceptanceEvent | MetricData - type: 'generation' | 'acceptance' | 'metric' - sandbox: sinon.SinonSandbox -}) { - await new Promise((resolve) => setTimeout(resolve, 100)) - params.sandbox.assert.calledWith(params.spy, params.sandbox.match(params.expectedEvent), params.type) -} - -export async function updateFilePaths( - session: Session, - content: string, - uploadId: string, - docScheme: string, - workspaceFolder: any -) { - const updatedFilePaths: NewFileInfo[] = [ - { - zipFilePath: path.normalize('README.md'), - relativePath: path.normalize('README.md'), - fileContent: content, - rejected: false, - virtualMemoryUri: generateVirtualMemoryUri(uploadId, path.normalize('README.md'), docScheme), - workspaceFolder: workspaceFolder, - changeApplied: false, - }, - ] - - Object.defineProperty(session.state, 'filePaths', { - get: () => updatedFilePaths, - configurable: true, - }) -} - -export const FollowUpSequences = { - generateReadme: [FollowUpTypes.NewTask, FollowUpTypes.CreateDocumentation, FollowUpTypes.ProceedFolderSelection], - updateReadme: [ - FollowUpTypes.NewTask, - FollowUpTypes.UpdateDocumentation, - FollowUpTypes.SynchronizeDocumentation, - FollowUpTypes.ProceedFolderSelection, - ], - editReadme: [ - FollowUpTypes.NewTask, - FollowUpTypes.UpdateDocumentation, - FollowUpTypes.EditDocumentation, - FollowUpTypes.ProceedFolderSelection, - ], - makeChanges: [FollowUpTypes.MakeChanges], - acceptContent: [FollowUpTypes.AcceptChanges], -} diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts deleted file mode 100644 index 7848d0561b0..00000000000 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ /dev/null @@ -1,717 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import * as assert from 'assert' -import * as path from 'path' -import sinon from 'sinon' -import { waitUntil } from '../../../../shared/utilities/timeoutUtils' -import { ControllerSetup, createController, createSession, generateVirtualMemoryUri } from '../../../amazonq/utils' -import { - CurrentWsFolders, - DeletedFileInfo, - MetricDataOperationName, - MetricDataResult, - NewFileInfo, -} from '../../../../amazonq/commons/types' -import { Session } from '../../../../amazonqFeatureDev/session/session' -import { Prompter } from '../../../../shared/ui/prompter' -import { assertTelemetry, toFile } from '../../../testUtil' -import { - CodeIterationLimitError, - ContentLengthError, - createUserFacingErrorMessage, - FeatureDevServiceError, - getMetricResult, - MonthlyConversationLimitError, - NoChangeRequiredException, - PrepareRepoFailedError, - PromptRefusalException, - SelectedFolderNotInWorkspaceFolderError, - TabIdNotFoundError, - UploadCodeError, - UploadURLExpired, - UserMessageNotFoundError, - ZipFileError, -} from '../../../../amazonqFeatureDev/errors' -import { - FeatureDevCodeGenState, - FeatureDevPrepareCodeGenState, -} from '../../../../amazonqFeatureDev/session/sessionState' -import { FeatureDevClient } from '../../../../amazonqFeatureDev/client/featureDev' -import { createAmazonQUri } from '../../../../amazonq/commons/diff' -import { AuthUtil } from '../../../../codewhisperer' -import { featureDevScheme, featureName, messageWithConversationId } from '../../../../amazonqFeatureDev' -import { i18n } from '../../../../shared/i18n-helper' -import { FollowUpTypes } from '../../../../amazonq/commons/types' -import { ToolkitError } from '../../../../shared' -import { MessengerTypes } from '../../../../amazonqFeatureDev/controllers/chat/messenger/constants' - -let mockGetCodeGeneration: sinon.SinonStub -describe('Controller', () => { - const tabID = '123' - const conversationID = '456' - const uploadID = '789' - - let session: Session - let controllerSetup: ControllerSetup - - const getFilePaths = (controllerSetup: ControllerSetup): NewFileInfo[] => [ - { - zipFilePath: 'myfile1.js', - relativePath: 'myfile1.js', - fileContent: '', - rejected: false, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'myfile1.js', featureDevScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - { - zipFilePath: 'myfile2.js', - relativePath: 'myfile2.js', - fileContent: '', - rejected: true, - virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'myfile2.js', featureDevScheme), - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ] - - const getDeletedFiles = (): DeletedFileInfo[] => [ - { - zipFilePath: 'myfile3.js', - relativePath: 'myfile3.js', - rejected: false, - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - { - zipFilePath: 'myfile4.js', - relativePath: 'myfile4.js', - rejected: true, - workspaceFolder: controllerSetup.workspaceFolder, - changeApplied: false, - }, - ] - - async function createCodeGenState() { - mockGetCodeGeneration = sinon.stub().resolves({ codeGenerationStatus: { status: 'Complete' } }) - - const workspaceFolders = [controllerSetup.workspaceFolder] as CurrentWsFolders - const testConfig = { - conversationId: conversationID, - proxyClient: { - createConversation: () => sinon.stub(), - createUploadUrl: () => sinon.stub(), - generatePlan: () => sinon.stub(), - startCodeGeneration: () => sinon.stub(), - getCodeGeneration: () => mockGetCodeGeneration(), - exportResultArchive: () => sinon.stub(), - } as unknown as FeatureDevClient, - workspaceRoots: [''], - uploadId: uploadID, - workspaceFolders, - } - - const codeGenState = new FeatureDevCodeGenState(testConfig, getFilePaths(controllerSetup), [], [], tabID, 0, {}) - const newSession = await createSession({ - messenger: controllerSetup.messenger, - sessionState: codeGenState, - conversationID, - tabID, - uploadID, - scheme: featureDevScheme, - }) - return newSession - } - - before(() => { - sinon.stub(performance, 'now').returns(0) - }) - - beforeEach(async () => { - controllerSetup = await createController() - session = await createSession({ - messenger: controllerSetup.messenger, - conversationID, - tabID, - uploadID, - scheme: featureDevScheme, - }) - - sinon.stub(AuthUtil.instance, 'getChatAuthState').resolves({ - codewhispererCore: 'connected', - codewhispererChat: 'connected', - amazonQ: 'connected', - }) - }) - - afterEach(() => { - sinon.restore() - }) - - describe('openDiff', async () => { - async function openDiff(filePath: string, deleted = false) { - const executeDiff = sinon.stub(vscode.commands, 'executeCommand').returns(Promise.resolve(undefined)) - controllerSetup.emitters.openDiff.fire({ tabID, conversationID, filePath, deleted }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(executeDiff.callCount > 0) - }, {}) - - return executeDiff - } - - it('uses empty file when file is not found locally', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const executedDiff = await openDiff(path.join('src', 'mynewfile.js')) - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - createAmazonQUri('empty', tabID, featureDevScheme), - createAmazonQUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - - it('uses file location when file is found locally and /src is not available', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'mynewfile.js') - await toFile('', newFileLocation) - const executedDiff = await openDiff('mynewfile.js') - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - vscode.Uri.file(newFileLocation), - createAmazonQUri(path.join(uploadID, 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - - it('uses file location when file is found locally and /src is available', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src', 'mynewfile.js') - await toFile('', newFileLocation) - const executedDiff = await openDiff(path.join('src', 'mynewfile.js')) - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - vscode.Uri.file(newFileLocation), - createAmazonQUri(path.join(uploadID, 'src', 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - - it('uses file location when file is found locally and source folder was picked', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const newFileLocation = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'foo', 'fi', 'mynewfile.js') - await toFile('', newFileLocation) - sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder) - session.config.workspaceRoots = [path.join(controllerSetup.workspaceFolder.uri.fsPath, 'foo', 'fi')] - const executedDiff = await openDiff(path.join('foo', 'fi', 'mynewfile.js')) - assert.strictEqual( - executedDiff.calledWith( - 'vscode.diff', - vscode.Uri.file(newFileLocation), - createAmazonQUri(path.join(uploadID, 'foo', 'fi', 'mynewfile.js'), tabID, featureDevScheme) - ), - true - ) - - assertTelemetry('amazonq_isReviewedChanges', { amazonqConversationId: conversationID, enabled: true }) - }) - }) - - describe('modifyDefaultSourceFolder', () => { - async function modifyDefaultSourceFolder(sourceRoot: string) { - const promptStub = sinon.stub(Prompter.prototype, 'prompt').resolves(vscode.Uri.file(sourceRoot)) - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { - type: FollowUpTypes.ModifyDefaultSourceFolder, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(promptStub.callCount > 0) - }, {}) - - return controllerSetup.sessionStorage.getSession(tabID) - } - - it('fails if selected folder is not under a workspace folder', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(undefined) - const messengerSpy = sinon.spy(controllerSetup.messenger, 'sendAnswer') - await modifyDefaultSourceFolder('../../') - assert.deepStrictEqual( - messengerSpy.calledWith({ - tabID, - type: 'answer', - message: new SelectedFolderNotInWorkspaceFolderError().message, - canBeVoted: true, - }), - true - ) - assert.deepStrictEqual( - messengerSpy.calledWith({ - tabID, - type: 'system-prompt', - followUps: sinon.match.any, - }), - true - ) - }) - - it('accepts valid source folders under a workspace root', async () => { - sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder) - const expectedSourceRoot = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src') - const modifiedSession = await modifyDefaultSourceFolder(expectedSourceRoot) - assert.strictEqual(modifiedSession.config.workspaceRoots.length, 1) - assert.strictEqual(modifiedSession.config.workspaceRoots[0], expectedSourceRoot) - }) - }) - - describe('newTask', () => { - async function newTaskClicked() { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { - type: FollowUpTypes.NewTask, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - } - - it('end chat telemetry is sent', async () => { - await newTaskClicked() - - assertTelemetry('amazonq_endChat', { amazonqConversationId: conversationID, result: 'Succeeded' }) - }) - }) - - describe('fileClicked', () => { - async function fileClicked( - getSessionStub: sinon.SinonStub<[tabID: string], Promise>, - action: string, - filePath: string - ) { - controllerSetup.emitters.fileClicked.fire({ - tabID, - conversationID, - filePath, - action, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - return getSessionStub.getCall(0).returnValue - } - - it('clicking the "Reject File" button updates the file state to "rejected: true"', async () => { - const filePath = getFilePaths(controllerSetup)[0].zipFilePath - const session = await createCodeGenState() - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - const rejectFile = await fileClicked(getSessionStub, 'reject-change', filePath) - assert.strictEqual(rejectFile.state.filePaths?.find((i) => i.relativePath === filePath)?.rejected, true) - }) - - it('clicking the "Reject File" button and then "Revert Reject File", updates the file state to "rejected: false"', async () => { - const filePath = getFilePaths(controllerSetup)[0].zipFilePath - const session = await createCodeGenState() - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - await fileClicked(getSessionStub, 'reject-change', filePath) - const revertRejection = await fileClicked(getSessionStub, 'revert-rejection', filePath) - assert.strictEqual( - revertRejection.state.filePaths?.find((i) => i.relativePath === filePath)?.rejected, - false - ) - }) - }) - - describe('insertCode', () => { - it('sets the number of files accepted counting also deleted files', async () => { - async function insertCode() { - const initialState = new FeatureDevPrepareCodeGenState( - { - conversationId: conversationID, - proxyClient: new FeatureDevClient(), - workspaceRoots: [''], - workspaceFolders: [controllerSetup.workspaceFolder], - uploadId: uploadID, - }, - getFilePaths(controllerSetup), - getDeletedFiles(), - [], - tabID, - 0 - ) - - const newSession = await createSession({ - messenger: controllerSetup.messenger, - sessionState: initialState, - conversationID, - tabID, - uploadID, - scheme: featureDevScheme, - }) - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(newSession) - - controllerSetup.emitters.followUpClicked.fire({ - tabID, - conversationID, - followUp: { - type: FollowUpTypes.InsertCode, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - } - - await insertCode() - - assertTelemetry('amazonq_isAcceptedCodeChanges', { - amazonqConversationId: conversationID, - amazonqNumberOfFilesAccepted: 2, - enabled: true, - result: 'Succeeded', - }) - }) - }) - - describe('processUserChatMessage', function () { - // TODO: fix disablePreviousFileList error - const runs = [ - { name: 'ContentLengthError', error: new ContentLengthError() }, - { - name: 'MonthlyConversationLimitError', - error: new MonthlyConversationLimitError('Service Quota Exceeded'), - }, - { - name: 'FeatureDevServiceErrorGuardrailsException', - error: new FeatureDevServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'GuardrailsException' - ), - }, - { - name: 'FeatureDevServiceErrorEmptyPatchException', - error: new FeatureDevServiceError( - i18n('AWS.amazonq.featureDev.error.throttling'), - 'EmptyPatchException' - ), - }, - { - name: 'FeatureDevServiceErrorThrottlingException', - error: new FeatureDevServiceError( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - 'ThrottlingException' - ), - }, - { name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') }, - { name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() }, - { name: 'TabIdNotFoundError', error: new TabIdNotFoundError() }, - { name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() }, - { name: 'PromptRefusalException', error: new PromptRefusalException() }, - { name: 'ZipFileError', error: new ZipFileError() }, - { name: 'CodeIterationLimitError', error: new CodeIterationLimitError() }, - { name: 'UploadURLExpired', error: new UploadURLExpired() }, - { name: 'NoChangeRequiredException', error: new NoChangeRequiredException() }, - { name: 'default', error: new ToolkitError('Default', { code: 'Default' }) }, - ] - - async function fireChatMessage(session: Session) { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - controllerSetup.emitters.processHumanChatMessage.fire({ - tabID, - conversationID, - message: 'test message', - }) - - /** - * Wait until the controller has time to process the event - * Sessions should be called twice: - * 1. When the session getWorkspaceRoot is called - * 2. When the controller processes preloader - */ - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 1) - }, {}) - } - - describe('onCodeGeneration', function () { - let session: any - let sendMetricDataTelemetrySpy: sinon.SinonStub - - async function verifyException(error: ToolkitError) { - sinon.stub(session, 'send').throws(error) - - await fireChatMessage(session) - await verifyMetricsCalled() - assert.ok( - sendMetricDataTelemetrySpy.calledWith( - MetricDataOperationName.StartCodeGeneration, - MetricDataResult.Success - ) - ) - const metricResult = getMetricResult(error) - assert.ok( - sendMetricDataTelemetrySpy.calledWith(MetricDataOperationName.EndCodeGeneration, metricResult) - ) - } - - async function verifyMetricsCalled() { - await waitUntil(() => Promise.resolve(sendMetricDataTelemetrySpy.callCount >= 2), {}) - } - - async function verifyMessage( - expectedMessage: string, - type: MessengerTypes, - remainingIterations?: number, - totalIterations?: number - ) { - sinon.stub(session, 'send').resolves() - sinon.stub(session, 'sendLinesOfCodeGeneratedTelemetry').resolves() // Avoid sending extra telemetry - const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer') - sinon.stub(session.state, 'codeGenerationRemainingIterationCount').value(remainingIterations) - sinon.stub(session.state, 'codeGenerationTotalIterationCount').value(totalIterations) - - await fireChatMessage(session) - await verifyMetricsCalled() - - assert.ok( - sendAnswerSpy.calledWith({ - type, - tabID, - message: expectedMessage, - }) - ) - } - - beforeEach(async () => { - session = await createCodeGenState() - sinon.stub(session, 'preloader').resolves() - sendMetricDataTelemetrySpy = sinon.stub(session, 'sendMetricDataTelemetry') - }) - - it('sends success operation telemetry', async () => { - sinon.stub(session, 'send').resolves() - sinon.stub(session, 'sendLinesOfCodeGeneratedTelemetry').resolves() // Avoid sending extra telemetry - - await fireChatMessage(session) - await verifyMetricsCalled() - - assert.ok( - sendMetricDataTelemetrySpy.calledWith( - MetricDataOperationName.StartCodeGeneration, - MetricDataResult.Success - ) - ) - assert.ok( - sendMetricDataTelemetrySpy.calledWith( - MetricDataOperationName.EndCodeGeneration, - MetricDataResult.Success - ) - ) - }) - - for (const { name, error } of runs) { - it(`sends failure operation telemetry on ${name}`, async () => { - await verifyException(error) - }) - } - - // Using 3 to avoid spamming the tests - for (let remainingIterations = 0; remainingIterations <= 3; remainingIterations++) { - it(`verifies add code messages for remaining iterations at ${remainingIterations}`, async () => { - const totalIterations = 10 - const expectedMessage = (() => { - if (remainingIterations > 2) { - return 'Would you like me to add this code to your project, or provide feedback for new code?' - } else if (remainingIterations > 0) { - return `Would you like me to add this code to your project, or provide feedback for new code? You have ${remainingIterations} out of ${totalIterations} code generations left.` - } else { - return 'Would you like me to add this code to your project?' - } - })() - await verifyMessage(expectedMessage, 'answer', remainingIterations, totalIterations) - }) - } - - for (let remainingIterations = -1; remainingIterations <= 3; remainingIterations++) { - let remaining: number | undefined = remainingIterations - if (remainingIterations < 0) { - remaining = undefined - } - it(`verifies messages after cancellation for remaining iterations at ${remaining !== undefined ? remaining : 'undefined'}`, async () => { - const totalIterations = 10 - const expectedMessage = (() => { - if (remaining === undefined || remaining > 2) { - return 'I stopped generating your code. If you want to continue working on this task, provide another description.' - } else if (remaining > 0) { - return `I stopped generating your code. If you want to continue working on this task, provide another description. You have ${remaining} out of ${totalIterations} code generations left.` - } else { - return "I stopped generating your code. You don't have more iterations left, however, you can start a new session." - } - })() - session.state.tokenSource.cancel() - await verifyMessage( - expectedMessage, - 'answer-part', - remaining, - remaining === undefined ? undefined : totalIterations - ) - }) - } - }) - - describe('processErrorChatMessage', function () { - function createTestErrorMessage(message: string) { - return createUserFacingErrorMessage(`${featureName} request failed: ${message}`) - } - - async function verifyException(error: ToolkitError) { - sinon.stub(session, 'preloader').throws(error) - const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer') - const sendErrorMessageSpy = sinon.stub(controllerSetup.messenger, 'sendErrorMessage') - const sendMonthlyLimitErrorSpy = sinon.stub(controllerSetup.messenger, 'sendMonthlyLimitError') - - await fireChatMessage(session) - - switch (error.constructor.name) { - case ContentLengthError.name: - assert.ok( - sendAnswerSpy.calledWith({ - type: 'answer', - tabID, - message: error.message + messageWithConversationId(session?.conversationIdUnsafe), - canBeVoted: true, - }) - ) - break - case MonthlyConversationLimitError.name: - assert.ok(sendMonthlyLimitErrorSpy.calledWith(tabID)) - break - case FeatureDevServiceError.name: - case UploadCodeError.name: - case UserMessageNotFoundError.name: - case TabIdNotFoundError.name: - case PrepareRepoFailedError.name: - assert.ok( - sendErrorMessageSpy.calledWith( - createTestErrorMessage(error.message), - tabID, - session?.retries, - session?.conversationIdUnsafe - ) - ) - break - case PromptRefusalException.name: - case ZipFileError.name: - assert.ok( - sendErrorMessageSpy.calledWith( - createTestErrorMessage(error.message), - tabID, - 0, - session?.conversationIdUnsafe, - true - ) - ) - break - case NoChangeRequiredException.name: - case CodeIterationLimitError.name: - case UploadURLExpired.name: - assert.ok( - sendAnswerSpy.calledWith({ - type: 'answer', - tabID, - message: error.message, - canBeVoted: true, - }) - ) - break - default: - assert.ok( - sendErrorMessageSpy.calledWith( - i18n('AWS.amazonq.featureDev.error.codeGen.default'), - tabID, - session?.retries, - session?.conversationIdUnsafe, - true - ) - ) - break - } - } - - for (const run of runs) { - it(`should handle ${run.name}`, async function () { - await verifyException(run.error) - }) - } - }) - }) - - describe('stopResponse', () => { - it('should emit ui_click telemetry with elementId amazonq_stopCodeGeneration', async () => { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - controllerSetup.emitters.stopResponse.fire({ tabID, conversationID }) - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - assertTelemetry('ui_click', { elementId: 'amazonq_stopCodeGeneration' }) - }) - }) - - describe('closeSession', async () => { - async function closeSessionClicked() { - const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - - controllerSetup.emitters.followUpClicked.fire({ - tabID, - followUp: { - type: FollowUpTypes.CloseSession, - }, - }) - - // Wait until the controller has time to process the event - await waitUntil(() => { - return Promise.resolve(getSessionStub.callCount > 0) - }, {}) - } - - it('end chat telemetry is sent', async () => { - await closeSessionClicked() - - assertTelemetry('amazonq_endChat', { amazonqConversationId: conversationID, result: 'Succeeded' }) - }) - }) -}) diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts deleted file mode 100644 index 2d68654ee00..00000000000 --- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode' -import assert from 'assert' -import sinon from 'sinon' -import { - MockCodeGenState, - FeatureDevPrepareCodeGenState, - FeatureDevCodeGenState, -} from '../../../amazonqFeatureDev/session/sessionState' -import { ToolkitError } from '../../../shared/errors' -import * as crypto from '../../../shared/crypto' -import { createMockSessionStateAction } from '../../amazonq/utils' - -import { createTestContext, setupTestHooks } from '../../amazonq/session/testSetup' - -describe('sessionStateFeatureDev', () => { - const context = createTestContext() - setupTestHooks(context) - - describe('MockCodeGenState', () => { - it('loops forever in the same state', async () => { - sinon.stub(crypto, 'randomUUID').returns('upload-id' as ReturnType<(typeof crypto)['randomUUID']>) - const testAction = createMockSessionStateAction() - const state = new MockCodeGenState(context.testConfig, context.tabId) - const result = await state.interact(testAction) - - assert.deepStrictEqual(result, { - nextState: state, - interaction: {}, - }) - }) - }) - - describe('FeatureDevPrepareCodeGenState', () => { - it('error when failing to prepare repo information', async () => { - sinon.stub(vscode.workspace, 'findFiles').throws() - context.testMocks.createUploadUrl!.resolves({ uploadId: '', uploadUrl: '' }) - const testAction = createMockSessionStateAction() - - await assert.rejects(() => { - return new FeatureDevPrepareCodeGenState(context.testConfig, [], [], [], context.tabId, 0).interact( - testAction - ) - }) - }) - }) - - describe('FeatureDevCodeGenState', () => { - it('transitions to FeatureDevPrepareCodeGenState when codeGenerationStatus ready ', async () => { - context.testMocks.getCodeGeneration!.resolves({ - codeGenerationStatus: { status: 'Complete' }, - codeGenerationRemainingIterationCount: 2, - codeGenerationTotalIterationCount: 3, - }) - - context.testMocks.exportResultArchive!.resolves({ newFileContents: [], deletedFiles: [], references: [] }) - - const testAction = createMockSessionStateAction() - const state = new FeatureDevCodeGenState(context.testConfig, [], [], [], context.tabId, 0, {}, 2, 3) - const result = await state.interact(testAction) - - const nextState = new FeatureDevPrepareCodeGenState( - context.testConfig, - [], - [], - [], - context.tabId, - 1, - 2, - 3, - undefined - ) - - assert.deepStrictEqual(result.nextState?.deletedFiles, nextState.deletedFiles) - assert.deepStrictEqual(result.nextState?.filePaths, result.nextState?.filePaths) - assert.deepStrictEqual(result.nextState?.references, result.nextState?.references) - }) - - it('fails when codeGenerationStatus failed ', async () => { - context.testMocks.getCodeGeneration!.rejects(new ToolkitError('Code generation failed')) - const testAction = createMockSessionStateAction() - const state = new FeatureDevCodeGenState(context.testConfig, [], [], [], context.tabId, 0, {}) - try { - await state.interact(testAction) - assert.fail('failed code generations should throw an error') - } catch (e: any) { - assert.deepStrictEqual(e.message, 'Code generation failed') - } - }) - }) -}) diff --git a/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts b/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts new file mode 100644 index 00000000000..1884e16e984 --- /dev/null +++ b/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts @@ -0,0 +1,79 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { SharedCredentialsProvider } from '../../../auth/providers/sharedCredentialsProvider' +import { createTestSections } from '../../credentials/testUtil' +import { DefaultStsClient } from '../../../shared/clients/stsClient' +import { oneDay } from '../../../shared/datetime' +import sinon from 'sinon' +import { SsoAccessTokenProvider } from '../../../auth/sso/ssoAccessTokenProvider' +import { SsoClient } from '../../../auth/sso/clients' + +describe('SharedCredentialsProvider - Role Chaining with SSO', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('should handle role chaining from SSO profile', async function () { + // Mock the SSO authentication + sandbox.stub(SsoAccessTokenProvider.prototype, 'getToken').resolves({ + accessToken: 'test-token', + expiresAt: new Date(Date.now() + oneDay), + }) + + // Mock SSO getRoleCredentials + sandbox.stub(SsoClient.prototype, 'getRoleCredentials').resolves({ + accessKeyId: 'sso-access-key', + secretAccessKey: 'sso-secret-key', + sessionToken: 'sso-session-token', + expiration: new Date(Date.now() + oneDay), + }) + + // Mock STS assumeRole + sandbox.stub(DefaultStsClient.prototype, 'assumeRole').callsFake(async (request) => { + assert.strictEqual(request.RoleArn, 'arn:aws:iam::123456789012:role/dev') + return { + Credentials: { + AccessKeyId: 'assumed-access-key', + SecretAccessKey: 'assumed-secret-key', + SessionToken: 'assumed-session-token', + Expiration: new Date(Date.now() + oneDay), + }, + } + }) + + const sections = await createTestSections(` + [sso-session aws1_session] + sso_start_url = https://example.awsapps.com/start + sso_region = us-east-1 + sso_registration_scopes = sso:account:access + + [profile Landing] + sso_session = aws1_session + sso_account_id = 111111111111 + sso_role_name = Landing + region = us-east-1 + + [profile dev] + region = us-east-1 + role_arn = arn:aws:iam::123456789012:role/dev + source_profile = Landing + `) + + const provider = new SharedCredentialsProvider('dev', sections) + const credentials = await provider.getCredentials() + + assert.strictEqual(credentials.accessKeyId, 'assumed-access-key') + assert.strictEqual(credentials.secretAccessKey, 'assumed-secret-key') + assert.strictEqual(credentials.sessionToken, 'assumed-session-token') + }) +}) diff --git a/packages/core/src/test/index.ts b/packages/core/src/test/index.ts index 9a01973e26d..c682b1f367e 100644 --- a/packages/core/src/test/index.ts +++ b/packages/core/src/test/index.ts @@ -22,6 +22,5 @@ export { getTestWorkspaceFolder } from '../testInteg/integrationTestsUtilities' export * from './codewhisperer/testUtil' export * from './credentials/testUtil' export * from './testUtil' -export * from './amazonq/utils' export * from './fake/mockFeatureConfigData' export * from './shared/ui/testUtils' diff --git a/packages/core/src/testInteg/perf/prepareRepoData.test.ts b/packages/core/src/testInteg/perf/prepareRepoData.test.ts deleted file mode 100644 index c1ba1df1223..00000000000 --- a/packages/core/src/testInteg/perf/prepareRepoData.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import assert from 'assert' -import * as sinon from 'sinon' -import { WorkspaceFolder } from 'vscode' -import { getEqualOSTestOptions, performanceTest } from '../../shared/performance/performance' -import { createTestWorkspace } from '../../test/testUtil' -import { prepareRepoData, TelemetryHelper } from '../../amazonqFeatureDev' -import { AmazonqCreateUpload, fs, getRandomString } from '../../shared' -import { Span } from '../../shared/telemetry' -import { FileSystem } from '../../shared/fs/fs' -import { getFsCallsUpperBound } from './utilities' - -type resultType = { - zipFileBuffer: Buffer - zipFileChecksum: string -} - -type setupResult = { - workspace: WorkspaceFolder - fsSpy: sinon.SinonSpiedInstance - numFiles: number - fileSize: number -} - -function performanceTestWrapper(numFiles: number, fileSize: number) { - return performanceTest( - getEqualOSTestOptions({ - userCpuUsage: 200, - systemCpuUsage: 35, - heapTotal: 20, - }), - `handles ${numFiles} files of size ${fileSize} bytes`, - function () { - const telemetry = new TelemetryHelper() - return { - setup: async () => { - const fsSpy = sinon.spy(fs) - const workspace = await createTestWorkspace(numFiles, { - fileNamePrefix: 'file', - fileContent: getRandomString(fileSize), - fileNameSuffix: '.md', - }) - return { workspace, fsSpy, numFiles, fileSize } - }, - execute: async (setup: setupResult) => { - return await prepareRepoData( - [setup.workspace.uri.fsPath], - [setup.workspace], - { - record: () => {}, - } as unknown as Span, - { telemetry } - ) - }, - verify: async (setup: setupResult, result: resultType) => { - verifyResult(setup, result, telemetry, numFiles * fileSize) - }, - } - } - ) -} - -function verifyResult(setup: setupResult, result: resultType, telemetry: TelemetryHelper, expectedSize: number): void { - assert.ok(result) - assert.strictEqual(Buffer.isBuffer(result.zipFileBuffer), true) - assert.strictEqual(telemetry.repositorySize, expectedSize) - assert.strictEqual(result.zipFileChecksum.length, 44) - assert.ok(getFsCallsUpperBound(setup.fsSpy) <= setup.numFiles * 8, 'total system calls should be under 8 per file') -} - -describe('prepareRepoData', function () { - describe('Performance Tests', function () { - afterEach(function () { - sinon.restore() - }) - performanceTestWrapper(10, 1000) - performanceTestWrapper(50, 500) - performanceTestWrapper(100, 100) - performanceTestWrapper(250, 10) - }) -}) diff --git a/packages/core/src/testInteg/perf/registerNewFiles.test.ts b/packages/core/src/testInteg/perf/registerNewFiles.test.ts deleted file mode 100644 index 716e79d4e48..00000000000 --- a/packages/core/src/testInteg/perf/registerNewFiles.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import assert from 'assert' -import sinon from 'sinon' -import * as vscode from 'vscode' -import { featureDevScheme } from '../../amazonqFeatureDev' -import { getEqualOSTestOptions, performanceTest } from '../../shared/performance/performance' -import { getTestWorkspaceFolder } from '../integrationTestsUtilities' -import { VirtualFileSystem } from '../../shared' -import { registerNewFiles } from '../../amazonq/util/files' -import { NewFileInfo, NewFileZipContents } from '../../amazonq' - -interface SetupResult { - workspace: vscode.WorkspaceFolder - fileContents: NewFileZipContents[] - vfsSpy: sinon.SinonSpiedInstance - vfs: VirtualFileSystem -} - -function getFileContents(numFiles: number, fileSize: number): NewFileZipContents[] { - return Array.from({ length: numFiles }, (_, i) => { - return { - zipFilePath: `test-path-${i}`, - fileContent: 'x'.repeat(fileSize), - } - }) -} - -function performanceTestWrapper(label: string, numFiles: number, fileSize: number) { - const conversationId = 'test-conversation' - return performanceTest( - getEqualOSTestOptions({ - userCpuUsage: 300, - systemCpuUsage: 35, - heapTotal: 20, - }), - label, - function () { - return { - setup: async () => { - const testWorkspaceUri = vscode.Uri.file(getTestWorkspaceFolder()) - const fileContents = getFileContents(numFiles, fileSize) - const vfs = new VirtualFileSystem() - const vfsSpy = sinon.spy(vfs) - - return { - workspace: { - uri: testWorkspaceUri, - name: 'test-workspace', - index: 0, - }, - fileContents: fileContents, - vfsSpy: vfsSpy, - vfs: vfs, - } - }, - execute: async (setup: SetupResult) => { - return registerNewFiles( - setup.vfs, - setup.fileContents, - 'test-upload-id', - [setup.workspace], - conversationId, - featureDevScheme - ) - }, - verify: async (setup: SetupResult, result: NewFileInfo[]) => { - assert.strictEqual(result.length, numFiles) - assert.ok( - setup.vfsSpy.registerProvider.callCount <= numFiles, - 'only register each file once in vfs' - ) - }, - } - } - ) -} - -describe('registerNewFiles', function () { - describe('performance tests', function () { - performanceTestWrapper('1x10MB', 1, 10000) - performanceTestWrapper('10x1000B', 10, 1000) - performanceTestWrapper('100x100B', 100, 100) - performanceTestWrapper('1000x10B', 1000, 10) - performanceTestWrapper('10000x1B', 10000, 1) - }) -})