diff --git a/package-lock.json b/package-lock.json index 2057a7eced1..74176a2cc4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "devDependencies": { "@aws-toolkits/telemetry": "^1.0.272", "@playwright/browser-chromium": "^1.43.1", + "@types/he": "^1.2.3", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", "@types/webpack-env": "^1.18.5", @@ -6949,6 +6950,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/he": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz", + "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "license": "MIT" @@ -18784,7 +18792,6 @@ "dependencies": { "aws-core-vscode": "file:../core/" }, - "devDependencies": {}, "engines": { "npm": "^10.1.0", "vscode": "^1.83.0" diff --git a/package.json b/package.json index 39f4fe478eb..335751d3ddc 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "devDependencies": { "@aws-toolkits/telemetry": "^1.0.272", "@playwright/browser-chromium": "^1.43.1", + "@types/he": "^1.2.3", "@types/vscode": "^1.68.0", "@types/vscode-webview": "^1.57.1", "@types/webpack-env": "^1.18.5", diff --git a/packages/amazonq/.changes/next-release/Breaking Change-912fe720-0d1b-40a3-8638-f9a4d2948904.json b/packages/amazonq/.changes/next-release/Breaking Change-912fe720-0d1b-40a3-8638-f9a4d2948904.json new file mode 100644 index 00000000000..037912f620f --- /dev/null +++ b/packages/amazonq/.changes/next-release/Breaking Change-912fe720-0d1b-40a3-8638-f9a4d2948904.json @@ -0,0 +1,4 @@ +{ + "type": "Breaking Change", + "description": "Change keybind for focusing chat to ctrl+win+i on Windows, ctrl+cmd+i on macOS and ctrl+meta+i on Linux" +} diff --git a/packages/amazonq/.changes/next-release/Feature-31cd9ad4-1e6b-4f05-b910-11c5059bd2fa.json b/packages/amazonq/.changes/next-release/Feature-31cd9ad4-1e6b-4f05-b910-11c5059bd2fa.json new file mode 100644 index 00000000000..39ed57954aa --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-31cd9ad4-1e6b-4f05-b910-11c5059bd2fa.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Use inline chat to select code and transform it with natural language instructions" +} diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index fbb68da3fde..02e7dfe318b 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -71,7 +71,6 @@ "dependencies": { "aws-core-vscode": "file:../core/" }, - "devDependencies": {}, "contributesComments": { "configuration": { "properties": "Any settings also defined in packages/core/package.json will override same-named settings in this file." @@ -600,9 +599,9 @@ "keybindings": [ { "command": "_aws.amazonq.focusChat.keybinding", - "win": "win+i", - "mac": "cmd+i", - "linux": "meta+i" + "win": "ctrl+win+i", + "mac": "ctrl+cmd+i", + "linux": "ctrl+meta+i" }, { "command": "aws.amazonq.explainCode", @@ -668,6 +667,23 @@ "key": "left", "command": "editor.action.inlineSuggest.showPrevious", "when": "inlineSuggestionVisible && !editorReadonly && aws.codewhisperer.connected" + }, + { + "command": "aws.amazonq.inline.waitForUserInput", + "win": "ctrl+i", + "mac": "cmd+i", + "linux": "ctrl+i", + "when": "editorTextFocus && aws.codewhisperer.connected" + }, + { + "command": "aws.amazonq.inline.waitForUserDecisionAcceptAll", + "key": "enter", + "when": "editorTextFocus && aws.codewhisperer.connected && amazonq.inline.codelensShortcutEnabled" + }, + { + "command": "aws.amazonq.inline.waitForUserDecisionRejectAll", + "key": "escape", + "when": "editorTextFocus && aws.codewhisperer.connected && amazonq.inline.codelensShortcutEnabled" } ], "icons": { @@ -678,341 +694,355 @@ "fontCharacter": "\\f1aa" } }, - "aws-amazonq-q-squid-ink": { + "aws-amazonq-q-inline-close": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ab" } }, - "aws-amazonq-q-white": { + "aws-amazonq-q-inline-close-inverse": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ac" } }, - "aws-amazonq-transform-arrow-dark": { + "aws-amazonq-q-squid-ink": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ad" } }, - "aws-amazonq-transform-arrow-light": { + "aws-amazonq-q-white": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ae" } }, - "aws-amazonq-transform-default-dark": { + "aws-amazonq-transform-arrow-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1af" } }, - "aws-amazonq-transform-default-light": { + "aws-amazonq-transform-arrow-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b0" } }, - "aws-amazonq-transform-dependencies-dark": { + "aws-amazonq-transform-default-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b1" } }, - "aws-amazonq-transform-dependencies-light": { + "aws-amazonq-transform-default-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b2" } }, - "aws-amazonq-transform-file-dark": { + "aws-amazonq-transform-dependencies-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b3" } }, - "aws-amazonq-transform-file-light": { + "aws-amazonq-transform-dependencies-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b4" } }, - "aws-amazonq-transform-logo": { + "aws-amazonq-transform-file-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b5" } }, - "aws-amazonq-transform-step-into-dark": { + "aws-amazonq-transform-file-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b6" } }, - "aws-amazonq-transform-step-into-light": { + "aws-amazonq-transform-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b7" } }, - "aws-amazonq-transform-variables-dark": { + "aws-amazonq-transform-step-into-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b8" } }, - "aws-amazonq-transform-variables-light": { + "aws-amazonq-transform-step-into-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b9" } }, - "aws-applicationcomposer-icon": { + "aws-amazonq-transform-variables-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ba" } }, - "aws-applicationcomposer-icon-dark": { + "aws-amazonq-transform-variables-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bb" } }, - "aws-apprunner-service": { + "aws-applicationcomposer-icon": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bc" } }, - "aws-cdk-logo": { + "aws-applicationcomposer-icon-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bd" } }, - "aws-cloudformation-stack": { + "aws-apprunner-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1be" } }, - "aws-cloudwatch-log-group": { + "aws-cdk-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bf" } }, - "aws-codecatalyst-logo": { + "aws-cloudformation-stack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c0" } }, - "aws-codewhisperer-icon-black": { + "aws-cloudwatch-log-group": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c1" } }, - "aws-codewhisperer-icon-white": { + "aws-codecatalyst-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c2" } }, - "aws-codewhisperer-learn": { + "aws-codewhisperer-icon-black": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c3" } }, - "aws-ecr-registry": { + "aws-codewhisperer-icon-white": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c4" } }, - "aws-ecs-cluster": { + "aws-codewhisperer-learn": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c5" } }, - "aws-ecs-container": { + "aws-ecr-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c6" } }, - "aws-ecs-service": { + "aws-ecs-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c7" } }, - "aws-generic-attach-file": { + "aws-ecs-container": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c8" } }, - "aws-iot-certificate": { + "aws-ecs-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c9" } }, - "aws-iot-policy": { + "aws-generic-attach-file": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ca" } }, - "aws-iot-thing": { + "aws-iot-certificate": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cb" } }, - "aws-lambda-function": { + "aws-iot-policy": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cc" } }, - "aws-mynah-MynahIconBlack": { + "aws-iot-thing": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cd" } }, - "aws-mynah-MynahIconWhite": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ce" } }, - "aws-mynah-logo": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cf" } }, - "aws-redshift-cluster": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d0" } }, - "aws-redshift-cluster-connected": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d1" } }, - "aws-redshift-database": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d2" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-redshift-schema": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-redshift-table": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-s3-bucket": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-s3-create-bucket": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-schemas-registry": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-schemas-schema": { + "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-stepfunctions-preview": { + "aws-schemas-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } + }, + "aws-schemas-schema": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1db" + } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1dc" + } } }, "walkthroughs": [ diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts index 2e77ef4c133..5edd9affdcd 100644 --- a/packages/amazonq/src/app/chat/activation.ts +++ b/packages/amazonq/src/app/chat/activation.ts @@ -9,11 +9,12 @@ import { telemetry } from 'aws-core-vscode/telemetry' import { AuthUtil, CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' import { Commands, placeholder, funcUtil } from 'aws-core-vscode/shared' import * as amazonq from 'aws-core-vscode/amazonq' +import { init as inlineChatInit } from '../../inlineChat/app' export async function activate(context: ExtensionContext) { const appInitContext = amazonq.DefaultAmazonQAppInitContext.instance - registerApps(appInitContext) + registerApps(appInitContext, context) const provider = new amazonq.AmazonQChatViewProvider( context, @@ -64,10 +65,11 @@ export async function activate(context: ExtensionContext) { void setupAuthNotification() } -function registerApps(appInitContext: amazonq.AmazonQAppInitContext) { +function registerApps(appInitContext: amazonq.AmazonQAppInitContext, context: ExtensionContext) { amazonq.cwChatAppInit(appInitContext) amazonq.featureDevChatAppInit(appInitContext) amazonq.gumbyChatAppInit(appInitContext) + inlineChatInit(context) } /** diff --git a/packages/amazonq/src/inlineChat/app.ts b/packages/amazonq/src/inlineChat/app.ts new file mode 100644 index 00000000000..f783ef8d84f --- /dev/null +++ b/packages/amazonq/src/inlineChat/app.ts @@ -0,0 +1,12 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { InlineChatController } from '../inlineChat/controller/inlineChatController' +import { registerInlineCommands } from '../inlineChat/command/registerInlineCommands' + +export function init(context: vscode.ExtensionContext) { + const inlineChatController = new InlineChatController(context) + registerInlineCommands(context, inlineChatController) +} diff --git a/packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts b/packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts new file mode 100644 index 00000000000..34a85e10b38 --- /dev/null +++ b/packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts @@ -0,0 +1,76 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as os from 'os' +import { InlineTask, TaskState } from '../controller/inlineTask' + +export class CodelensProvider implements vscode.CodeLensProvider { + private codeLenses: vscode.CodeLens[] = [] + private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter() + public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event + + constructor(context: vscode.ExtensionContext) { + context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', this)) + this.provideCodeLenses = this.provideCodeLenses.bind(this) + } + + public provideCodeLenses(_document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.CodeLens[] { + return this.codeLenses + } + + public updateLenses(task: InlineTask): void { + if (task.state === TaskState.Complete) { + this.codeLenses = [] + this._onDidChangeCodeLenses.fire() + return + } + switch (task.state) { + case TaskState.InProgress: { + this.codeLenses = [] + this.codeLenses.push( + new vscode.CodeLens(new vscode.Range(task.selectedRange.start, task.selectedRange.start), { + title: 'Amazon Q is generating...', + command: '', + }) + ) + break + } + case TaskState.WaitingForDecision: { + let acceptTitle: string + let rejectTitle: string + if (os.platform() === 'darwin') { + acceptTitle = 'Accept ($(newline))' + rejectTitle = `Reject ( \u238B )` + } else { + acceptTitle = 'Accept (Enter)' + rejectTitle = `Reject (Esc)` + } + + this.codeLenses = [] + this.codeLenses.push( + new vscode.CodeLens(new vscode.Range(task.selectedRange.start, task.selectedRange.start), { + title: acceptTitle, + command: 'aws.amazonq.inline.waitForUserDecisionAcceptAll', + arguments: [task], + }) + ) + this.codeLenses.push( + new vscode.CodeLens(new vscode.Range(task.selectedRange.start, task.selectedRange.start), { + title: rejectTitle, + command: 'aws.amazonq.inline.waitForUserDecisionRejectAll', + arguments: [task], + }) + ) + break + } + default: { + this.codeLenses = [] + break + } + } + this._onDidChangeCodeLenses.fire() + } +} diff --git a/packages/amazonq/src/inlineChat/command/registerInlineCommands.ts b/packages/amazonq/src/inlineChat/command/registerInlineCommands.ts new file mode 100644 index 00000000000..94c1e159f6e --- /dev/null +++ b/packages/amazonq/src/inlineChat/command/registerInlineCommands.ts @@ -0,0 +1,22 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import vscode from 'vscode' +import { InlineChatController } from '../controller/inlineChatController' +import { InlineTask } from '../controller/inlineTask' + +export function registerInlineCommands(context: vscode.ExtensionContext, inlineChatController: InlineChatController) { + context.subscriptions.push( + vscode.commands.registerCommand('aws.amazonq.inline.waitForUserInput', async () => { + await inlineChatController.inlineQuickPick() + }), + vscode.commands.registerCommand('aws.amazonq.inline.waitForUserDecisionAcceptAll', async (task: InlineTask) => { + await inlineChatController.acceptAllChanges(task, true) + }), + vscode.commands.registerCommand('aws.amazonq.inline.waitForUserDecisionRejectAll', async (task: InlineTask) => { + await inlineChatController.rejectAllChanges(task, true) + }) + ) +} diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts new file mode 100644 index 00000000000..bb9cf4b92e0 --- /dev/null +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -0,0 +1,393 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { randomUUID } from 'crypto' +import * as vscode from 'vscode' +import { InlineDecorator } from '../decorations/inlineDecorator' +import { InlineChatProvider } from '../provider/inlineChatProvider' +import { InlineTask, TaskState, TextDiff } from './inlineTask' +import { responseTransformer } from '../output/responseTransformer' +import { adjustTextDiffForEditing, computeDiff } from '../output/computeDiff' +import { computeDecorations } from '../decorations/computeDecorations' +import { CodelensProvider } from '../codeLenses/codeLenseProvider' +import { ReferenceLogController } from 'aws-core-vscode/codewhispererChat' +import { CodeWhispererSettings } from 'aws-core-vscode/codewhisperer' +import { codicon, getIcon, getLogger, messages, setContext, Timeout, textDocumentUtil } from 'aws-core-vscode/shared' +import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' + +export class InlineChatController { + private task: InlineTask | undefined + private readonly decorator = new InlineDecorator() + private readonly inlineChatProvider: InlineChatProvider + private readonly codeLenseProvider: CodelensProvider + private readonly referenceLogController = new ReferenceLogController() + private readonly inlineLineAnnotationController: InlineLineAnnotationController + private userQuery: string | undefined + private listeners: vscode.Disposable[] = [] + + constructor(context: vscode.ExtensionContext) { + this.inlineChatProvider = new InlineChatProvider() + this.inlineChatProvider.onErrorOccured(() => this.handleError()) + this.codeLenseProvider = new CodelensProvider(context) + this.inlineLineAnnotationController = new InlineLineAnnotationController(context) + } + + public async createTask( + query: string, + document: vscode.TextDocument, + selectionRange: vscode.Selection + ): Promise { + const inlineTask = new InlineTask(query, document, selectionRange) + return inlineTask + } + + public async acceptAllChanges(task = this.task, userInvoked: boolean): Promise { + if (!task) { + return + } + const editor = vscode.window.visibleTextEditors.find( + (editor) => editor.document.uri.toString() === task.document.uri.toString() + ) + if (!editor) { + return + } + if (userInvoked) { + this.inlineChatProvider.sendTelemetryEvent( + { + userDecision: 'ACCEPT', + }, + this.task + ) + } + const deletions = task.diff.filter((diff) => diff.type === 'deletion') + await editor.edit( + (editBuilder) => { + for (const deletion of deletions) { + editBuilder.delete(deletion.range) + } + }, + { undoStopAfter: false, undoStopBefore: false } + ) + task.diff = [] + task.updateDecorations() + this.decorator.applyDecorations(task) + await this.updateTaskAndLenses(task) + this.referenceLogController.addReferenceLog(task.codeReferences, task.replacement ? task.replacement : '') + await this.reset() + } + + public async rejectAllChanges(task = this.task, userInvoked: boolean): Promise { + if (!task) { + return + } + const editor = vscode.window.visibleTextEditors.find( + (editor) => editor.document.uri.toString() === task.document.uri.toString() + ) + if (!editor) { + return + } + if (userInvoked) { + this.inlineChatProvider.sendTelemetryEvent( + { + userDecision: 'REJECT', + }, + this.task + ) + } + const insertions = task.diff.filter((diff) => diff.type === 'insertion') + await editor.edit( + (editBuilder) => { + for (const insertion of insertions) { + editBuilder.delete(insertion.range) + } + }, + { undoStopAfter: false, undoStopBefore: false } + ) + task.diff = [] + task.updateDecorations() + this.decorator.applyDecorations(task) + await this.updateTaskAndLenses(task) + this.referenceLogController.addReferenceLog(task.codeReferences, task.replacement ? task.replacement : '') + await this.reset() + } + + public async updateTaskAndLenses(task: InlineTask, taskState?: TaskState) { + if (taskState) { + task.state = taskState + } else if (!task.diff || task.diff.length === 0) { + // If the previous state was waiting for a decision and the code diff is clean, then we mark the task as completed + if (task.state === TaskState.WaitingForDecision) { + task.state = TaskState.Complete + } + } + this.codeLenseProvider.updateLenses(task) + if (task.state === TaskState.InProgress) { + if (vscode.window.activeTextEditor) { + await this.inlineLineAnnotationController.hide(vscode.window.activeTextEditor) + } + } + await this.refreshCodeLenses(task) + if (task.state === TaskState.Complete) { + await this.reset() + } + } + + private async handleError() { + if (!this.task) { + return + } + this.task.state = TaskState.Error + this.codeLenseProvider.updateLenses(this.task) + await this.refreshCodeLenses(this.task) + await this.reset() + } + + private async reset() { + this.listeners.forEach((listener) => listener.dispose()) + this.listeners = [] + + this.task = undefined + this.inlineLineAnnotationController.enable() + await setContext('amazonq.inline.codelensShortcutEnabled', undefined) + } + + private async refreshCodeLenses(task: InlineTask): Promise { + await vscode.commands.executeCommand('vscode.executeCodeLensProvider', task.document.uri) + } + + public async inlineQuickPick(previouseQuery?: string) { + const editor = vscode.window.activeTextEditor + if (!editor) { + return + } + + if (this.task && this.task.isActiveState()) { + void vscode.window.showWarningMessage( + 'Amazon Q: Reject or Accept the current suggestion before creating a new one' + ) + return + } + + await vscode.window + .showInputBox({ + value: previouseQuery ?? '', + placeHolder: 'Enter instructions for Q', + prompt: codicon`${getIcon('aws-amazonq-q-white')} Edit code`, + }) + .then(async (query) => { + this.userQuery = query + if (!query) { + return + } + await textDocumentUtil.addEofNewline(editor) + this.task = await this.createTask(query, editor.document, editor.selection) + await this.inlineLineAnnotationController.disable(editor) + await this.computeDiffAndRenderOnEditor(query, editor.document).catch(async (err) => { + getLogger().error(err) + if (err instanceof Error) { + void vscode.window.showErrorMessage(`Amazon Q: ${err.message}`) + } else { + void vscode.window.showErrorMessage('Amazon Q encountered an error') + } + await this.handleError() + }) + }) + } + + private async computeDiffAndRenderOnEditor(query: string, document: vscode.TextDocument) { + if (!this.task) { + return + } + + await this.updateTaskAndLenses(this.task, TaskState.InProgress) + const prompt = query + getLogger().info(`prompt:\n${prompt}`) + const uuid = randomUUID() + const message = { + message: prompt, + messageId: uuid, + command: undefined, + userIntent: undefined, + tabID: uuid, + } + + const requestStart = performance.now() + let responseStartLatency: number | undefined + + const response = await this.inlineChatProvider.processPromptMessage(message) + this.task.requestId = response?.$metadata.requestId + + // Deselect all code + const editor = vscode.window.activeTextEditor + if (editor) { + const selection = editor.selection + if (!selection.isEmpty) { + const cursor = selection.active + const newSelection = new vscode.Selection(cursor, cursor) + editor.selection = newSelection + } + } + + if (response) { + let qSuggestedCodeResponse = '' + for await (const chatEvent of response.generateAssistantResponseResponse!) { + if ( + chatEvent.assistantResponseEvent?.content !== undefined && + chatEvent.assistantResponseEvent.content.length > 0 + ) { + if (responseStartLatency === undefined) { + responseStartLatency = performance.now() - requestStart + } + + qSuggestedCodeResponse += chatEvent.assistantResponseEvent.content + + const transformedResponse = responseTransformer(qSuggestedCodeResponse, this.task, false) + if (transformedResponse) { + const textDiff = computeDiff(transformedResponse, this.task, true) + const decorations = computeDecorations(this.task) + this.task.decorations = decorations + await this.applyDiff(this.task!, textDiff ?? [], { + undoStopBefore: false, + undoStopAfter: false, + }) + this.decorator.applyDecorations(this.task) + this.task.previouseDiff = textDiff + } + } + if ( + chatEvent.codeReferenceEvent?.references !== undefined && + chatEvent.codeReferenceEvent.references.length > 0 + ) { + this.task.codeReferences = this.task.codeReferences.concat(chatEvent.codeReferenceEvent?.references) + // clear diff if user settings is off for code reference + if (!CodeWhispererSettings.instance.isSuggestionsWithCodeReferencesEnabled()) { + await this.rejectAllChanges(this.task, false) + void vscode.window.showInformationMessage( + 'Your settings do not allow code generation with references.' + ) + await this.updateTaskAndLenses(this.task, TaskState.Complete) + return + } + } + if (chatEvent.error) { + await this.rejectAllChanges(this.task, false) + void vscode.window.showErrorMessage(`Amazon Q: ${chatEvent.error.message}`) + await this.updateTaskAndLenses(this.task, TaskState.Complete) + return + } + } + + if (this.task) { + // Unclear why we need to check if task is defined, but occasionally an error occurs otherwise + this.task.responseStartLatency = responseStartLatency + this.task.responseEndLatency = performance.now() - requestStart + } + getLogger().info(`qSuggestedCodeResponse:\n${qSuggestedCodeResponse}`) + const transformedResponse = responseTransformer(qSuggestedCodeResponse, this.task, true) + if (transformedResponse) { + const textDiff = computeDiff(transformedResponse, this.task, false) + const decorations = computeDecorations(this.task) + this.task.decorations = decorations + await this.applyDiff(this.task, textDiff ?? []) + this.decorator.applyDecorations(this.task) + await this.updateTaskAndLenses(this.task, TaskState.WaitingForDecision) + await setContext('amazonq.inline.codelensShortcutEnabled', true) + this.undoListener(this.task) + } else { + void messages.showMessageWithCancel( + 'No suggestions from Q, please try different instructions.', + new Timeout(5000) + ) + await this.updateTaskAndLenses(this.task, TaskState.Complete) + await this.inlineQuickPick(this.userQuery) + await this.handleError() + } + } + } + + private async applyDiff( + task: InlineTask, + textDiff: TextDiff[], + undoOption?: { undoStopBefore: boolean; undoStopAfter: boolean } + ) { + const adjustedTextDiff = adjustTextDiffForEditing(textDiff) + const visibleEditor = vscode.window.visibleTextEditors.find( + (editor) => editor.document.uri === task.document.uri + ) + const previousDiff = task.previouseDiff?.filter((diff) => diff.type === 'insertion') + + if (visibleEditor) { + if (previousDiff) { + await visibleEditor.edit( + (editBuilder) => { + for (const insertion of previousDiff) { + editBuilder.delete(insertion.range) + } + }, + { undoStopAfter: false, undoStopBefore: false } + ) + } + await visibleEditor.edit( + (editBuilder) => { + for (const change of adjustedTextDiff) { + if (change.type === 'insertion') { + editBuilder.insert(change.range.start, change.replacementText) + } + } + }, + undoOption ?? { undoStopBefore: true, undoStopAfter: false } + ) + } else { + if (previousDiff) { + const edit = new vscode.WorkspaceEdit() + for (const insertion of previousDiff) { + edit.delete(task.document.uri, insertion.range) + } + await vscode.workspace.applyEdit(edit) + } + const edit = new vscode.WorkspaceEdit() + for (const change of textDiff) { + if (change.type === 'insertion') { + edit.insert(task.document.uri, change.range.start, change.replacementText) + } + } + await vscode.workspace.applyEdit(edit) + } + } + + private undoListener(task: InlineTask) { + const listener: vscode.Disposable = vscode.workspace.onDidChangeTextDocument(async (event) => { + const { document, contentChanges } = event + + if (document.uri.toString() !== task.document.uri.toString()) { + return + } + + const changeIntersectsRange = contentChanges.some((change) => { + const { range } = change + if (task.selectedRange) { + return !( + range.end.isBefore(task.selectedRange.start) || range.start.isAfter(task.selectedRange.end) + ) + } + }) + + if (!changeIntersectsRange) { + return + } + + const updatedSelectedText = document.getText(task.selectedRange) + + if (updatedSelectedText.trim() === task.selectedText.trim()) { + task.diff = [] + await this.updateTaskAndLenses(task) + task.updateDecorations() + this.decorator.applyDecorations(task) + listener.dispose() + } + }) + + this.listeners.push(listener) + } +} diff --git a/packages/amazonq/src/inlineChat/controller/inlineTask.ts b/packages/amazonq/src/inlineChat/controller/inlineTask.ts new file mode 100644 index 00000000000..a6a169ad58c --- /dev/null +++ b/packages/amazonq/src/inlineChat/controller/inlineTask.ts @@ -0,0 +1,160 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import type { CodeReference } from 'aws-core-vscode/amazonq' +import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' +import type { Decorations } from '../decorations/inlineDecorator' +import { computeDecorations } from '../decorations/computeDecorations' +import { extractLanguageNameFromFile } from 'aws-core-vscode/codewhispererChat' +import { textDocumentUtil } from 'aws-core-vscode/shared' + +interface TextToInsert { + type: 'insertion' + replacementText: string + range: vscode.Range +} + +interface TextToDelete { + type: 'deletion' + originalText: string + range: vscode.Range +} + +interface DiffBlock { + originalText: string + replacementText: string + range: vscode.Range +} + +export type TextDiff = TextToInsert | TextToDelete + +export enum TaskState { + Idle = 'Idle', + InProgress = 'InProgress', + WaitingForDecision = 'WaitingForDecision', + Complete = 'Complete', + Error = 'Error', +} + +export class InlineTask { + public state: TaskState = TaskState.Idle + public diff: TextDiff[] = [] + public decorations: Decorations | undefined + public diffBlock: DiffBlock[] = [] + public codeReferences: CodeReference[] = [] + public selectedText: string + public languageName: string | undefined + + public partialSelectedText: string | undefined + public partialSelectedTextRight: string | undefined + + public previouseDiff: TextDiff[] | undefined + public selectedRange: vscode.Range + public inProgressReplacement: string | undefined + public replacement: string | undefined + + // Telemetry fields + public requestId?: string + public responseStartLatency?: number + public responseEndLatency?: number + + constructor( + public query: string, + public document: vscode.TextDocument, + selection: vscode.Selection + ) { + this.selectedRange = textDocumentUtil.expandSelectionToFullLines(document, selection) + this.selectedText = document.getText(this.selectedRange) + this.languageName = extractLanguageNameFromFile(document) + } + + public revertDiff(): void { + this.diff = [] + this.decorations = { + linesAdded: [], + linesRemoved: [], + } + } + + public removeDiffChangeByRange(range: vscode.Range): void { + if (this.diff) { + this.diff = this.diff.filter((change) => !change.range.isEqual(range)) + } + } + + public updateDecorations(): void { + const isEmpty = + !this.decorations || + (this.decorations?.linesAdded?.length === 0 && this.decorations?.linesRemoved?.length === 0) + + if (isEmpty) { + return + } + const updatedDecorations = computeDecorations(this) + this.decorations = updatedDecorations + } + + public updateDiff(affectedRange: vscode.Range, deletedLines: number) { + const diffsAfter = this.diff.filter((edit) => edit.range.start.isAfter(affectedRange.end)) + for (const diff of diffsAfter) { + diff.range = new vscode.Range( + diff.range.start.translate(-deletedLines), + diff.range.end.translate(-deletedLines) + ) + } + } + + // Telemetry methods + public get numSelectedLines() { + return this.selectedText.split('\n').length + } + + public get inputLength() { + return this.query.length + } + + public inlineChatEventBase() { + let numSuggestionAddChars = 0 + let numSuggestionAddLines = 0 + let numSuggestionDelChars = 0 + let numSuggestionDelLines = 0 + + for (const diff of this.diff) { + if (diff.type === 'insertion') { + numSuggestionAddChars += diff.replacementText.length + numSuggestionAddLines += diff.range.end.line - diff.range.start.line + 1 + } else { + numSuggestionDelChars += diff.originalText.length + numSuggestionDelLines += diff.range.end.line - diff.range.start.line + 1 + } + } + + const programmingLanguage = this.languageName + ? { + languageName: this.languageName, + } + : undefined + + const event: Partial = { + requestId: this.requestId, + timestamp: new Date(), + inputLength: this.inputLength, + numSelectedLines: this.numSelectedLines, + codeIntent: true, + responseStartLatency: this.responseStartLatency, + responseEndLatency: this.responseEndLatency, + numSuggestionAddChars, + numSuggestionAddLines, + numSuggestionDelChars, + numSuggestionDelLines, + programmingLanguage, + } + return event + } + + public isActiveState() { + return !(this.state === TaskState.Complete || this.state === TaskState.Error) + } +} diff --git a/packages/amazonq/src/inlineChat/decorations/computeDecorations.ts b/packages/amazonq/src/inlineChat/decorations/computeDecorations.ts new file mode 100644 index 00000000000..fb289494b3f --- /dev/null +++ b/packages/amazonq/src/inlineChat/decorations/computeDecorations.ts @@ -0,0 +1,32 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { InlineTask } from '../controller/inlineTask' +import { Decorations } from './inlineDecorator' + +export function computeDecorations(task: InlineTask): Decorations | undefined { + if (!task.diff) { + return + } + + const decorations: Decorations = { + linesAdded: [], + linesRemoved: [], + } + + for (const edit of task.diff) { + const countChanged = edit.range.end.line - edit.range.start.line - 1 + if (edit.type === 'deletion') { + decorations.linesRemoved.push({ + range: new vscode.Range(edit.range.start.line, 0, edit.range.start.line + countChanged, 0), + }) + } else if (edit.type === 'insertion') { + decorations.linesAdded.push({ + range: new vscode.Range(edit.range.start.line, 0, edit.range.start.line + countChanged, 0), + }) + } + } + return decorations +} diff --git a/packages/amazonq/src/inlineChat/decorations/inlineDecorator.ts b/packages/amazonq/src/inlineChat/decorations/inlineDecorator.ts new file mode 100644 index 00000000000..3c4e3899e13 --- /dev/null +++ b/packages/amazonq/src/inlineChat/decorations/inlineDecorator.ts @@ -0,0 +1,38 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { InlineTask } from '../controller/inlineTask' + +export interface Decorations { + linesAdded: vscode.DecorationOptions[] + linesRemoved: vscode.DecorationOptions[] +} + +const removedTextDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 0, 0, 0.1)', + isWholeLine: true, +}) + +const AddedTextDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(0, 255, 0, 0.1)', + isWholeLine: true, +}) + +export class InlineDecorator { + public applyDecorations(task: InlineTask): void { + const decorations = task.decorations + if (!decorations) { + return + } + const editors = vscode.window.visibleTextEditors.filter( + (editor) => editor.document.uri.toString() === task.document.uri.toString() + ) + for (const editor of editors) { + editor.setDecorations(AddedTextDecorationType, decorations.linesAdded ?? []) + editor.setDecorations(removedTextDecorationType, decorations.linesRemoved ?? []) + } + } +} diff --git a/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts b/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts new file mode 100644 index 00000000000..9ec5e08122d --- /dev/null +++ b/packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts @@ -0,0 +1,57 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Container } from 'aws-core-vscode/codewhisperer' +import * as vscode from 'vscode' + +export class InlineLineAnnotationController { + private enabled: boolean = true + + constructor(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.window.onDidChangeTextEditorSelection(async ({ selections, textEditor }) => { + let showShow = false + + if (this.enabled) { + for (const selection of selections) { + if (selection.end.line === selection.start.line + 1 && selection.end.character === 0) { + // dont show if the selection is just a newline + } else if (selection.start.line !== selection.end.line) { + showShow = true + break + } + } + } + + await this.setVisible(textEditor, showShow) + }, this) + ) + } + + private async setVisible(editor: vscode.TextEditor, visible: boolean) { + let needsRefresh: boolean + if (visible) { + needsRefresh = await Container.instance.lineAnnotationController.tryShowInlineHint() + } else { + needsRefresh = await Container.instance.lineAnnotationController.tryHideInlineHint() + } + if (needsRefresh) { + await Container.instance.lineAnnotationController.refresh(editor, 'codewhisperer') + } + } + + async hide(editor: vscode.TextEditor) { + await this.setVisible(editor, false) + } + + enable() { + this.enabled = true + } + + async disable(editor: vscode.TextEditor) { + this.enabled = false + await this.setVisible(editor, false) + } +} diff --git a/packages/amazonq/src/inlineChat/output/computeDiff.ts b/packages/amazonq/src/inlineChat/output/computeDiff.ts new file mode 100644 index 00000000000..9b599b2eff3 --- /dev/null +++ b/packages/amazonq/src/inlineChat/output/computeDiff.ts @@ -0,0 +1,116 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { type LinesOptions, diffLines, Change } from 'diff' +import * as vscode from 'vscode' +import { InlineTask, TextDiff } from '../controller/inlineTask' + +export function computeDiff(response: string, inlineTask: InlineTask, isPartialDiff: boolean): TextDiff[] | undefined { + if (!response) { + return + } + const selectedRange = inlineTask.selectedRange + const partialSelectedText = inlineTask.partialSelectedText ?? '' + const selectedText = isPartialDiff ? partialSelectedText : inlineTask.selectedText + + const normalizedResponse = + getLeadingWhitespace(selectedText) + response.trim() + getTrailingWhitespace(selectedText) + + const diffs = diffLines(selectedText, normalizedResponse, { + stripTrailingCr: true, + ignoreNewlineAtEof: true, + } as LinesOptions) + + const textDiff: TextDiff[] = [] + let startLine = selectedRange.start.line + + diffs.forEach((part: Change) => { + const count = part.count ?? 0 + if (part.removed) { + if (part.value !== '\n') { + textDiff.push({ + type: 'deletion', + originalText: part.value, + range: new vscode.Range(startLine, 0, startLine + count, 0), + }) + } + } else if (part.added) { + if (part.value !== '\n') { + // The partial response sometimes doesn't have the correct ending newline character (\n), so we ensure that every insertion respects the code formatting. + if (isPartialDiff && !part.value.endsWith('\n')) { + part.value += '\n' + } + textDiff.push({ + type: 'insertion', + replacementText: part.value, + range: new vscode.Range(startLine, 0, startLine + count, 0), + }) + } + } + startLine += count + }) + inlineTask.diff = textDiff + return textDiff +} + +export function adjustTextDiffForEditing(textDiff: TextDiff[]): TextDiff[] { + let linesAdded = 0 + const adjustedDiff: TextDiff[] = [] + + for (const edit of textDiff) { + const { range, type } = edit + const { start, end } = range + const linesChanged = end.line - start.line + + const adjustedRange = new vscode.Range( + new vscode.Position(start.line - linesAdded, start.character), + new vscode.Position(end.line - linesAdded, end.character) + ) + + adjustedDiff.push({ + ...edit, + range: adjustedRange, + }) + + if (type === 'insertion') { + linesAdded += linesChanged + } + } + + return adjustedDiff +} + +export function getDiffBlocks(inlineTask: InlineTask): vscode.Range[] { + const diff = inlineTask.diff + + if (!diff || diff.length === 0) { + return [] + } + + const diffBlocks: vscode.Range[] = [] + let currentRange: vscode.Range | undefined + + for (const change of diff) { + const { range } = change + if (!currentRange || range.start.line !== currentRange.end.line) { + currentRange = range + diffBlocks.push(range) + } else { + currentRange = new vscode.Range(currentRange.start, range.end) + diffBlocks[diffBlocks.length - 1] = currentRange + } + } + + return diffBlocks +} + +function getLeadingWhitespace(str: string): string { + const match = str.match(/^\s*/) + return match ? match[0] : '' +} + +function getTrailingWhitespace(str: string): string { + const match = str.match(/\s*$/) + return match ? match[0] : '' +} diff --git a/packages/amazonq/src/inlineChat/output/responseTransformer.ts b/packages/amazonq/src/inlineChat/output/responseTransformer.ts new file mode 100644 index 00000000000..b965ced519e --- /dev/null +++ b/packages/amazonq/src/inlineChat/output/responseTransformer.ts @@ -0,0 +1,55 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLogger } from 'aws-core-vscode/shared' +import { decode } from 'he' +import { InlineTask } from '../controller/inlineTask' + +/** + * Transforms the response from the INLINE_CHAT GenerateAssistantResponse call. + * + * @param response - The raw response string from GenerateAssistantResponse. + * @param inlineTask - The inline task object containing information about the current task. + * @param isWholeResponse - A boolean indicating whether this is a complete response or a partial one. + * @returns The decoded response string, or undefined if an error occurs. + */ +export function responseTransformer( + response: string, + inlineTask: InlineTask, + isWholeResponse: boolean +): string | undefined { + try { + const decodedResponse = decode(response) + if (!isWholeResponse) { + const [partialSelectedCode, right] = extractPartialCode(decodedResponse, inlineTask) + inlineTask.partialSelectedText = partialSelectedCode + inlineTask.partialSelectedTextRight = right + return decodedResponse + } else { + return decodedResponse + } + } catch (err) { + getLogger().error('An unknown error occurred: %s', (err as Error).message) + return undefined + } +} + +/** + * This function is used to handle partial responses in inline tasks. It divides + * the selected text into two parts: + * 1. The "left" part, which contains the same number of lines as the response. + * 2. The "right" part, which contains the remaining lines. + * + * @param response - The response string from the assistant. + * @param inlineTask - The inline task object containing the full selected text. + * @returns A tuple with two strings: [leftPart, rightPart]. + */ +function extractPartialCode(response: string, inlineTask: InlineTask): [string, string] { + const lineCount = response.split('\n').length + const splitLines = inlineTask.selectedText.split('\n') + const left = splitLines.slice(0, lineCount).join('\n') + const right = splitLines.slice(lineCount).join('\n') + return [left, right] +} diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts new file mode 100644 index 00000000000..91fbad54f93 --- /dev/null +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -0,0 +1,186 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { + CodeWhispererStreamingServiceException, + GenerateAssistantResponseCommandOutput, +} from '@amzn/codewhisperer-streaming' +import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { + ChatSessionStorage, + ChatTriggerType, + EditorContextExtractor, + PromptMessage, + TriggerEventsStorage, + TriggerPayload, + triggerPayloadToChatRequest, + UserIntentRecognizer, +} from 'aws-core-vscode/codewhispererChat' +import { AwsClientResponseError, getLogger, isAwsError, ToolkitError } from 'aws-core-vscode/shared' +import { randomUUID } from 'crypto' +import { codeWhispererClient } from 'aws-core-vscode/codewhisperer' +import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' +import { InlineTask } from '../controller/inlineTask' +import { extractAuthFollowUp } from 'aws-core-vscode/amazonq' + +export class InlineChatProvider { + private readonly editorContextExtractor: EditorContextExtractor + private readonly userIntentRecognizer: UserIntentRecognizer + private readonly sessionStorage: ChatSessionStorage + private readonly triggerEventsStorage: TriggerEventsStorage + private errorEmitter = new vscode.EventEmitter() + public onErrorOccured = this.errorEmitter.event + + public constructor() { + this.editorContextExtractor = new EditorContextExtractor() + this.userIntentRecognizer = new UserIntentRecognizer() + this.sessionStorage = new ChatSessionStorage() + this.triggerEventsStorage = new TriggerEventsStorage() + } + + public async processPromptMessage(message: PromptMessage) { + return this.editorContextExtractor + .extractContextForTrigger('ChatMessage') + .then((context) => { + const triggerID = randomUUID() + this.triggerEventsStorage.addTriggerEvent({ + id: triggerID, + tabID: message.tabID, + message: message.message, + type: 'inline_chat', + context, + }) + return this.generateResponse( + { + message: message.message, + trigger: ChatTriggerType.InlineChatMessage, + query: message.message, + codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock, + fileText: context?.focusAreaContext?.extendedCodeBlock, + fileLanguage: context?.activeFileContext?.fileLanguage, + filePath: context?.activeFileContext?.filePath, + matchPolicy: context?.activeFileContext?.matchPolicy, + codeQuery: context?.focusAreaContext?.names, + userIntent: this.userIntentRecognizer.getFromPromptChatMessage(message), + customization: getSelectedCustomization(), + }, + triggerID + ) + }) + .catch((e) => { + this.processException(e, message.tabID) + }) + } + + private async generateResponse( + triggerPayload: TriggerPayload & { projectContextQueryLatencyMs?: number }, + triggerID: string + ) { + const triggerEvent = this.triggerEventsStorage.getTriggerEvent(triggerID) + if (triggerEvent === undefined) { + return + } + + if (triggerEvent.tabID === 'no-available-tabs') { + return + } + + if (triggerEvent.tabID === undefined) { + setTimeout(() => { + this.generateResponse(triggerPayload, triggerID).catch((e) => { + getLogger().error('generateResponse failed: %s', (e as Error).message) + }) + }, 20) + return + } + + const tabID = triggerEvent.tabID + + const credentialsState = await AuthUtil.instance.getChatAuthState() + if ( + !(credentialsState.codewhispererChat === 'connected' && credentialsState.codewhispererCore === 'connected') + ) { + const { message } = extractAuthFollowUp(credentialsState) + this.errorEmitter.fire() + throw new ToolkitError(message) + } + triggerPayload.useRelevantDocuments = false + + const request = triggerPayloadToChatRequest(triggerPayload) + const session = this.sessionStorage.getSession(tabID) + getLogger().info( + `request from tab: ${tabID} conversationID: ${session.sessionIdentifier} request: ${JSON.stringify( + request + )}` + ) + + let response: GenerateAssistantResponseCommandOutput | undefined = undefined + session.createNewTokenSource() + try { + response = await session.chat(request) + getLogger().info( + `response to tab: ${tabID} conversationID: ${session.sessionIdentifier} requestID: ${response.$metadata.requestId} metadata: ${JSON.stringify( + response.$metadata + )}` + ) + } catch (e: any) { + this.processException(e, tabID) + } + + return response + } + + private processException(e: any, tabID: string) { + let errorMessage: string | undefined + let requestID: string | undefined + if (typeof e === 'string') { + errorMessage = e.toUpperCase() + } else if (e instanceof SyntaxError) { + // Workaround to handle case when LB returns web-page with error and our client doesn't return proper exception + errorMessage = AwsClientResponseError.tryExtractReasonFromSyntaxError(e) + } else if (e instanceof CodeWhispererStreamingServiceException) { + errorMessage = e.message + requestID = e.$metadata.requestId + } else if (e instanceof Error) { + errorMessage = e.message + } + + this.errorEmitter.fire() + this.sessionStorage.deleteSession(tabID) + + throw ToolkitError.chain(e, errorMessage ?? 'Failed to get response', { + details: { + tabID, + requestID, + }, + }) + } + + public sendTelemetryEvent(inlineChatEvent: InlineChatEvent, currentTask?: InlineTask) { + codeWhispererClient + .sendTelemetryEvent({ + telemetryEvent: { + inlineChatEvent: { + ...inlineChatEvent, + ...(currentTask?.inlineChatEventBase() ?? {}), + }, + }, + }) + .then() + .catch((error) => { + let requestId: string | undefined + if (isAwsError(error)) { + requestId = error.requestId + } + + getLogger().debug( + `Failed to sendTelemetryEvent to CodeWhisperer, requestId: ${ + requestId ?? '' + }, message: ${error.message}` + ) + }) + } +} diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 2900cd921ef..d015bfb6122 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -28,6 +28,9 @@ export { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/status export { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands' export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens' export { createAmazonQUri, openDiff, openDeletedDiff, getOriginalFileUri, getFileDiffUris } from './commons/diff' +export { CodeReference } from '../codewhispererChat/view/connector/connector' +export { AuthMessageDataMap, AuthFollowUpType } from './auth/model' +export { extractAuthFollowUp } from './util/authUtils' import { FeatureContext } from '../shared' /** diff --git a/packages/core/src/amazonq/util/authUtils.ts b/packages/core/src/amazonq/util/authUtils.ts new file mode 100644 index 00000000000..0fd48ebc4c9 --- /dev/null +++ b/packages/core/src/amazonq/util/authUtils.ts @@ -0,0 +1,40 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FeatureAuthState } from '../../codewhisperer' +import { AuthFollowUpType, AuthMessageDataMap } from '../auth/model' + +/** + * This function evaluates the authentication state of CodeWhisperer features (chat and core) + * when the authentication is not valid, and returns the appropriate authentication follow-up type and message. + * + * @param credentialState - The current authentication state for each CodeWhisperer feature + * @returns An object containing: + * - authType: The type of authentication follow-up required (AuthFollowUpType) + * - message: The corresponding message for the determined auth type + */ +export function extractAuthFollowUp(credentialState: FeatureAuthState) { + let authType: AuthFollowUpType = 'full-auth' + let message = AuthMessageDataMap[authType].message + if (credentialState.codewhispererChat === 'disconnected' && credentialState.codewhispererCore === 'disconnected') { + authType = 'full-auth' + message = AuthMessageDataMap[authType].message + } + + if (credentialState.codewhispererCore === 'connected' && credentialState.codewhispererChat === 'expired') { + authType = 'missing_scopes' + message = AuthMessageDataMap[authType].message + } + + if (credentialState.codewhispererChat === 'expired' && credentialState.codewhispererCore === 'expired') { + authType = 're-auth' + message = AuthMessageDataMap[authType].message + } + + return { + authType, + message, + } as const +} diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 3a94931dddb..9defffe1063 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -39,6 +39,7 @@ "output": { "shape": "CreateTaskAssistConversationResponse" }, "errors": [ { "shape": "ThrottlingException" }, + { "shape": "ServiceQuotaExceededException" }, { "shape": "InternalServerException" }, { "shape": "ValidationException" }, { "shape": "AccessDeniedException" } @@ -56,6 +57,7 @@ "errors": [ { "shape": "ThrottlingException" }, { "shape": "ConflictException" }, + { "shape": "ServiceQuotaExceededException" }, { "shape": "ResourceNotFoundException" }, { "shape": "InternalServerException" }, { "shape": "ValidationException" }, @@ -278,6 +280,7 @@ "errors": [ { "shape": "ThrottlingException" }, { "shape": "ConflictException" }, + { "shape": "ServiceQuotaExceededException" }, { "shape": "ResourceNotFoundException" }, { "shape": "InternalServerException" }, { "shape": "ValidationException" }, @@ -336,6 +339,53 @@ "documentation": "

Reason for AccessDeniedException

", "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] }, + "AppStudioState": { + "type": "structure", + "required": ["namespace", "propertyName", "propertyContext"], + "members": { + "namespace": { + "shape": "AppStudioStateNamespaceString", + "documentation": "

The namespace of the context. Examples: 'ui.Button', 'ui.Table.DataSource', 'ui.Table.RowActions.Button', 'logic.invokeAWS', 'logic.JavaScript'

" + }, + "propertyName": { + "shape": "AppStudioStatePropertyNameString", + "documentation": "

The name of the property. Examples: 'visibility', 'disability', 'value', 'code'

" + }, + "propertyValue": { + "shape": "AppStudioStatePropertyValueString", + "documentation": "

The value of the property.

" + }, + "propertyContext": { + "shape": "AppStudioStatePropertyContextString", + "documentation": "

Context about how the property is used

" + } + }, + "documentation": "

Description of a user's context when they are calling Q Chat from AppStudio

" + }, + "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 + }, "ArtifactId": { "type": "string", "max": 126, @@ -544,7 +594,8 @@ "members": { "programmingLanguage": { "shape": "ProgrammingLanguage" }, "codeScanJobId": { "shape": "CodeScanJobId" }, - "timestamp": { "shape": "Timestamp" } + "timestamp": { "shape": "Timestamp" }, + "codeAnalysisScope": { "shape": "CodeAnalysisScope" } } }, "CodeScanJobId": { @@ -612,6 +663,18 @@ "documentation": "

This exception is thrown when the action to perform could not be completed because the resource is in a conflicting state.

", "exception": true }, + "ConsoleState": { + "type": "structure", + "members": { + "region": { "shape": "String" }, + "consoleUrl": { "shape": "SensitiveString" }, + "serviceId": { "shape": "String" }, + "serviceConsolePage": { "shape": "String" }, + "serviceSubconsolePage": { "shape": "String" }, + "taskName": { "shape": "SensitiveString" } + }, + "documentation": "

Information about the state of the AWS management console page from which the user is calling

" + }, "ContentChecksumType": { "type": "string", "enum": ["SHA_256"] @@ -700,9 +763,7 @@ "uploadId": { "shape": "UploadId" }, "uploadUrl": { "shape": "PreSignedUrl" }, "kmsKeyArn": { "shape": "ResourceArn" }, - "requestHeaders": { - "shape": "RequestHeaders" - } + "requestHeaders": { "shape": "RequestHeaders" } } }, "CursorState": { @@ -860,6 +921,14 @@ "cursorState": { "shape": "CursorState", "documentation": "

Position of the cursor

" + }, + "relevantDocuments": { + "shape": "RelevantDocumentList", + "documentation": "

Represents IDE provided relevant files

" + }, + "useRelevantDocuments": { + "shape": "Boolean", + "documentation": "

Whether service should use relevant document in prompt

" } }, "documentation": "

Represents the state of an Editor

" @@ -927,6 +996,13 @@ "max": 100, "min": 0 }, + "FeatureDevEvent": { + "type": "structure", + "required": ["conversationId"], + "members": { + "conversationId": { "shape": "ConversationId" } + } + }, "FeatureEvaluation": { "type": "structure", "required": ["feature", "variation", "value"], @@ -1029,7 +1105,8 @@ "supplementalContexts": { "shape": "SupplementalContextList" }, "customizationArn": { "shape": "CustomizationArn" }, "optOutPreference": { "shape": "OptOutPreference" }, - "userContext": { "shape": "UserContext" } + "userContext": { "shape": "UserContext" }, + "profileArn": { "shape": "ProfileArn" } } }, "GenerateCompletionsRequestMaxResultsInteger": { @@ -1143,7 +1220,7 @@ }, "IdeCategory": { "type": "string", - "enum": ["JETBRAINS", "VSCODE", "CLI"], + "enum": ["JETBRAINS", "VSCODE", "CLI", "JUPYTER_MD", "JUPYTER_SM"], "max": 64, "min": 1 }, @@ -1170,6 +1247,29 @@ "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 @@ -1313,6 +1413,12 @@ "sensitive": true }, "PrimitiveInteger": { "type": "integer" }, + "ProfileArn": { + "type": "string", + "max": 950, + "min": 0, + "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" + }, "ProgrammingLanguage": { "type": "structure", "required": ["languageName"], @@ -1325,7 +1431,7 @@ "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)" + "pattern": "(python|javascript|java|csharp|typescript|c|cpp|go|kotlin|php|ruby|rust|scala|shell|sql|json|yaml|vue|tf|tsx|jsx|plaintext)" }, "ProgressUpdates": { "type": "list", @@ -1401,11 +1507,46 @@ "max": 10, "min": 0 }, - "ResourceArn": { + "RelevantDocumentList": { + "type": "list", + "member": { "shape": "RelevantTextDocument" }, + "max": 5, + "min": 0 + }, + "RelevantTextDocument": { + "type": "structure", + "required": ["relativeFilePath"], + "members": { + "relativeFilePath": { + "shape": "RelevantTextDocumentRelativeFilePathString", + "documentation": "

Filepath relative to the root of the workspace

" + }, + "programmingLanguage": { + "shape": "ProgrammingLanguage", + "documentation": "

The text document's language identifier.

" + }, + "text": { + "shape": "RelevantTextDocumentTextString", + "documentation": "

Content of the text document

" + }, + "documentSymbols": { + "shape": "DocumentSymbols", + "documentation": "

DocumentSymbols parsed from a text document

" + } + }, + "documentation": "

Represents an IDE retrieved relevant Text Document / File

" + }, + "RelevantTextDocumentRelativeFilePathString": { "type": "string", - "max": 1224, + "max": 4096, + "min": 1, + "sensitive": true + }, + "RelevantTextDocumentTextString": { + "type": "string", + "max": 10240, "min": 0, - "pattern": "arn:([-.a-z0-9]{1,63}:){2}([-.a-z0-9]{0,63}:){2}([a-zA-Z0-9-_:/]){1,1023}" + "sensitive": true }, "RequestHeaderKey": { "type": "string", @@ -1419,14 +1560,17 @@ }, "RequestHeaders": { "type": "map", - "key": { - "shape": "RequestHeaderKey" - }, - "value": { - "shape": "RequestHeaderValue" - }, + "key": { "shape": "RequestHeaderKey" }, + "value": { "shape": "RequestHeaderValue" }, "max": 16, - "min": 1 + "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", @@ -1495,7 +1639,8 @@ }, "telemetryEvent": { "shape": "TelemetryEvent" }, "optOutPreference": { "shape": "OptOutPreference" }, - "userContext": { "shape": "UserContext" } + "userContext": { "shape": "UserContext" }, + "profileArn": { "shape": "ProfileArn" } } }, "SendTelemetryEventResponse": { @@ -1506,6 +1651,15 @@ "type": "string", "sensitive": true }, + "ServiceQuotaExceededException": { + "type": "structure", + "required": ["message"], + "members": { + "message": { "shape": "String" } + }, + "documentation": "

This exception is thrown when request was denied due to caller exceeding their usage limits

", + "exception": true + }, "ShellHistory": { "type": "list", "member": { "shape": "ShellHistoryEntry" }, @@ -1609,11 +1763,11 @@ "members": { "artifacts": { "shape": "ArtifactMap" }, "programmingLanguage": { "shape": "ProgrammingLanguage" }, - "scope": { "shape": "CodeAnalysisScope" }, "clientToken": { "shape": "StartCodeAnalysisRequestClientTokenString", "idempotencyToken": true }, + "scope": { "shape": "CodeAnalysisScope" }, "codeScanName": { "shape": "CodeScanName" } } }, @@ -1641,7 +1795,9 @@ "required": ["conversationState", "workspaceState"], "members": { "conversationState": { "shape": "ConversationState" }, - "workspaceState": { "shape": "WorkspaceState" } + "workspaceState": { "shape": "WorkspaceState" }, + "taskAssistPlan": { "shape": "TaskAssistPlan" }, + "currentCodeGenerationId": { "shape": "CodeGenerationId" } }, "documentation": "

Structure to represent start code generation request.

" }, @@ -1770,6 +1926,63 @@ "type": "string", "enum": ["DECLARATION", "USAGE"] }, + "TaskAssistPlan": { + "type": "list", + "member": { "shape": "TaskAssistPlanStep" }, + "min": 0 + }, + "TaskAssistPlanStep": { + "type": "structure", + "required": ["filePath", "description"], + "members": { + "filePath": { + "shape": "TaskAssistPlanStepFilePathString", + "documentation": "

File path on which the step is working on.

" + }, + "description": { + "shape": "TaskAssistPlanStepDescriptionString", + "documentation": "

Description on the step.

" + }, + "startLine": { + "shape": "TaskAssistPlanStepStartLineInteger", + "documentation": "

Start line number of the related changes.

" + }, + "endLine": { + "shape": "TaskAssistPlanStepEndLineInteger", + "documentation": "

End line number of the related changes.

" + }, + "action": { + "shape": "TaskAssistPlanStepAction", + "documentation": "

Type of the action.

" + } + }, + "documentation": "

Structured plan step for a task assist plan.

" + }, + "TaskAssistPlanStepAction": { + "type": "string", + "documentation": "

Action for task assist plan step

", + "enum": ["MODIFY", "CREATE", "DELETE", "UNKNOWN"] + }, + "TaskAssistPlanStepDescriptionString": { + "type": "string", + "max": 1024, + "min": 1 + }, + "TaskAssistPlanStepEndLineInteger": { + "type": "integer", + "box": true, + "min": 0 + }, + "TaskAssistPlanStepFilePathString": { + "type": "string", + "max": 1024, + "min": 1 + }, + "TaskAssistPlanStepStartLineInteger": { + "type": "integer", + "box": true, + "min": 0 + }, "TaskAssistPlanningUploadContext": { "type": "structure", "required": ["conversationId"], @@ -1789,7 +2002,9 @@ "chatAddMessageEvent": { "shape": "ChatAddMessageEvent" }, "chatInteractWithMessageEvent": { "shape": "ChatInteractWithMessageEvent" }, "chatUserModificationEvent": { "shape": "ChatUserModificationEvent" }, - "terminalUserInteractionEvent": { "shape": "TerminalUserInteractionEvent" } + "terminalUserInteractionEvent": { "shape": "TerminalUserInteractionEvent" }, + "featureDevEvent": { "shape": "FeatureDevEvent" }, + "inlineChatEvent": { "shape": "InlineChatEvent" } }, "union": true }, @@ -1929,7 +2144,7 @@ }, "TransformationDownloadArtifactType": { "type": "string", - "enum": ["ClientInstructions", "Logs"] + "enum": ["ClientInstructions", "Logs", "GeneratedCode"] }, "TransformationDownloadArtifacts": { "type": "list", @@ -1962,7 +2177,15 @@ }, "TransformationLanguage": { "type": "string", - "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "C_SHARP"] + "enum": ["JAVA_8", "JAVA_11", "JAVA_17", "C_SHARP", "COBOL", "PL_I", "JCL"] + }, + "TransformationLanguages": { + "type": "list", + "member": { "shape": "TransformationLanguage" } + }, + "TransformationMainframeRuntimeEnv": { + "type": "string", + "enum": ["MAINFRAME"] }, "TransformationOperatingSystemFamily": { "type": "string", @@ -1995,24 +2218,40 @@ }, "TransformationProgressUpdateStatus": { "type": "string", - "enum": ["IN_PROGRESS", "COMPLETED", "FAILED", "PAUSED"] + "enum": ["IN_PROGRESS", "COMPLETED", "FAILED", "PAUSED", "AWAITING_CLIENT_ACTION"] + }, + "TransformationProjectArtifactDescriptor": { + "type": "structure", + "members": { + "sourceCodeArtifact": { "shape": "TransformationSourceCodeArtifactDescriptor" } + }, + "union": true }, "TransformationProjectState": { "type": "structure", "members": { "language": { "shape": "TransformationLanguage" }, "runtimeEnv": { "shape": "TransformationRuntimeEnv" }, - "platformConfig": { "shape": "TransformationPlatformConfig" } + "platformConfig": { "shape": "TransformationPlatformConfig" }, + "projectArtifact": { "shape": "TransformationProjectArtifactDescriptor" } } }, "TransformationRuntimeEnv": { "type": "structure", "members": { "java": { "shape": "TransformationJavaRuntimeEnv" }, - "dotNet": { "shape": "TransformationDotNetRuntimeEnv" } + "dotNet": { "shape": "TransformationDotNetRuntimeEnv" }, + "mainframe": { "shape": "TransformationMainframeRuntimeEnv" } }, "union": true }, + "TransformationSourceCodeArtifactDescriptor": { + "type": "structure", + "members": { + "languages": { "shape": "TransformationLanguages" }, + "runtimeEnv": { "shape": "TransformationRuntimeEnv" } + } + }, "TransformationSpec": { "type": "structure", "members": { @@ -2066,11 +2305,11 @@ }, "TransformationType": { "type": "string", - "enum": ["LANGUAGE_UPGRADE"] + "enum": ["LANGUAGE_UPGRADE", "DOCUMENT_GENERATION"] }, "TransformationUploadArtifactType": { "type": "string", - "enum": ["Dependencies"] + "enum": ["Dependencies", "ClientBuildResult"] }, "TransformationUploadContext": { "type": "structure", @@ -2173,11 +2412,23 @@ }, "envState": { "shape": "EnvState", - "documentation": "

Environment state chat messaage context.

" + "documentation": "

Environment state chat message context.

" + }, + "appStudioContext": { + "shape": "AppStudioState", + "documentation": "

The state of a user's AppStudio UI when sending a message.

" }, "diagnostic": { "shape": "Diagnostic", "documentation": "

Diagnostic chat message context.

" + }, + "consoleState": { + "shape": "ConsoleState", + "documentation": "

Contextual information about the environment from which the user is calling.

" + }, + "userSettings": { + "shape": "UserSettings", + "documentation": "

Settings information, e.g., whether the user has enabled cross-region API calls.

" } }, "documentation": "

Additional Chat message context associated with the Chat Message

" @@ -2192,7 +2443,8 @@ "SHOW_EXAMPLES", "CITE_SOURCES", "EXPLAIN_LINE_BY_LINE", - "EXPLAIN_CODE_SELECTION" + "EXPLAIN_CODE_SELECTION", + "GENERATE_CLOUDFORMATION_TEMPLATE" ] }, "UserModificationEvent": { @@ -2217,6 +2469,13 @@ "unmodifiedAcceptedCharacterCount": { "shape": "PrimitiveInteger" } } }, + "UserSettings": { + "type": "structure", + "members": { + "hasConsentedToCrossRegionCalls": { "shape": "Boolean" } + }, + "documentation": "

Settings information passed by the Q widget

" + }, "UserTriggerDecisionEvent": { "type": "structure", "required": [ diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index af056f8c76d..1c6423f7c86 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -25,6 +25,7 @@ export type { Completion, SendTelemetryEventResponse, TelemetryEvent, + InlineChatEvent, } from './client/codewhispereruserclient.d.ts' export type { default as CodeWhispererUserClient } from './client/codewhispereruserclient.d.ts' export { SecurityPanelViewProvider } from './views/securityPanelViewProvider' @@ -87,3 +88,5 @@ export * as supplementalContextUtil from './util/supplementalContext/supplementa export * from './service/diagnosticsProvider' export * as diagnosticsProvider from './service/diagnosticsProvider' export * from './ui/codeWhispererNodes' +export { getSelectedCustomization } from './util/customizationUtil' +export { Container } from './service/serviceContainer' diff --git a/packages/core/src/codewhisperer/views/lineAnnotationController.ts b/packages/core/src/codewhisperer/views/lineAnnotationController.ts index a75ec39a7e9..df00b6fe7ac 100644 --- a/packages/core/src/codewhisperer/views/lineAnnotationController.ts +++ b/packages/core/src/codewhisperer/views/lineAnnotationController.ts @@ -46,6 +46,7 @@ function fromId(id: string | undefined): AnnotationState | undefined { interface AnnotationState { id: string suppressWhileRunning: boolean + decorationRenderOptions?: vscode.ThemableDecorationAttachmentRenderOptions text: () => string updateState(changeSource: AnnotationChangeSource, force: boolean): AnnotationState | undefined @@ -199,6 +200,26 @@ export class EndState implements AnnotationState { } } +export class InlineChatState implements AnnotationState { + static static = 'amazonq_annotation_inline_chat' + id = InlineChatState.static + suppressWhileRunning = false + + text = () => { + if (os.platform() === 'darwin') { + return 'Amazon Q: Edit \u2318I' + } else { + return 'Amazon Q: Edit (Ctrl+I)' + } + } + updateState(_changeSource: AnnotationChangeSource, _force: boolean): AnnotationState { + return this + } + isNextState(_state: AnnotationState | undefined): boolean { + return false + } +} + /** * There are * - existing users @@ -314,6 +335,26 @@ export class LineAnnotationController implements vscode.Disposable { await globals.globalState.update(inlinehintKey, this._currentState.id) } + /** + * Trys to show the inline hint, if the tutorial is not finished it will not be shown + */ + async tryShowInlineHint(): Promise { + if (this.isTutorialDone()) { + this._isReady = true + this._currentState = new InlineChatState() + return true + } + return false + } + + async tryHideInlineHint(): Promise { + if (this._currentState instanceof InlineChatState) { + this._currentState = new EndState() + return true + } + return false + } + private async onActiveLinesChanged(e: LinesChangeEvent) { if (!this._isReady) { return @@ -438,7 +479,7 @@ export class LineAnnotationController implements vscode.Disposable { ): Partial | undefined { const isCWRunning = RecommendationService.instance.isRunning - const textOptions = { + const textOptions: vscode.ThemableDecorationAttachmentRenderOptions = { contentText: '', fontWeight: 'normal', fontStyle: 'normal', diff --git a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts index f853fb4611d..5d884b24d1a 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts @@ -11,7 +11,7 @@ import { SymbolType, TextDocument, } from '@amzn/codewhisperer-streaming' -import { TriggerPayload } from '../model' +import { ChatTriggerType, TriggerPayload } from '../model' import { undefinedIfEmpty } from '../../../../shared' const fqnNameSizeDownLimit = 1 @@ -98,6 +98,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): Gen const useRelevantDocuments = triggerPayload.useRelevantDocuments // service will throw validation exception if string is empty const customizationArn: string | undefined = undefinedIfEmpty(triggerPayload.customization.arn) + const chatTriggerType = triggerPayload.trigger === ChatTriggerType.InlineChatMessage ? 'INLINE_CHAT' : 'MANUAL' return { conversationState: { @@ -117,7 +118,7 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): Gen userIntent: triggerPayload.userIntent, }, }, - chatTriggerType: 'MANUAL', + chatTriggerType, customizationArn: customizationArn, }, } diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 41a31d30edd..c011d0e5c51 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -27,13 +27,13 @@ import { getHttpStatusCode, getRequestId, ToolkitError } from '../../../../share import { keys } from '../../../../shared/utilities/tsUtils' import { getLogger } from '../../../../shared/logger/logger' import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' -import { AuthFollowUpType, AuthMessageDataMap } from '../../../../amazonq/auth/model' import { userGuideURL } from '../../../../amazonq/webview/ui/texts/constants' import { CodeScanIssue } from '../../../../codewhisperer/models/model' import { marked } from 'marked' import { JSDOM } from 'jsdom' import { LspController } from '../../../../amazonq/lsp/lspController' import { extractCodeBlockLanguage } from '../../../../shared/markdown' +import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -44,26 +44,7 @@ export class Messenger { ) {} public async sendAuthNeededExceptionMessage(credentialState: FeatureAuthState, tabID: string, triggerID: string) { - let authType: AuthFollowUpType = 'full-auth' - let message = AuthMessageDataMap[authType].message - if ( - credentialState.codewhispererChat === 'disconnected' && - credentialState.codewhispererCore === 'disconnected' - ) { - authType = 'full-auth' - message = AuthMessageDataMap[authType].message - } - - if (credentialState.codewhispererCore === 'connected' && credentialState.codewhispererChat === 'expired') { - authType = 'missing_scopes' - message = AuthMessageDataMap[authType].message - } - - if (credentialState.codewhispererChat === 'expired' && credentialState.codewhispererCore === 'expired') { - authType = 're-auth' - message = AuthMessageDataMap[authType].message - } - + const { message, authType } = extractAuthFollowUp(credentialState) this.dispatcher.sendAuthNeededExceptionMessage( new AuthNeededException( { diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 4c269298fd9..f79510acacb 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -156,6 +156,7 @@ export interface ChatItemFeedbackMessage { export enum ChatTriggerType { ChatMessage = 'ChatMessage', + InlineChatMessage = 'InlineChatMessage', } export interface TriggerPayload { diff --git a/packages/core/src/codewhispererChat/index.ts b/packages/core/src/codewhispererChat/index.ts index 0a954458cfe..e473203caf5 100644 --- a/packages/core/src/codewhispererChat/index.ts +++ b/packages/core/src/codewhispererChat/index.ts @@ -7,3 +7,11 @@ export { FocusAreaContextExtractor } from './editor/context/focusArea/focusAreaE export { TryChatCodeLensProvider, resolveModifierKey, tryChatCodeLensCommand } from './editor/codelens' export { focusAmazonQPanel } from './commands/registerCommands' export { ChatSession } from './clients/chat/v0/chat' +export { triggerPayloadToChatRequest } from './controllers/chat/chatRequest/converter' +export { ChatTriggerType, PromptMessage, TriggerPayload } from './controllers/chat/model' +export { UserIntentRecognizer } from './controllers/chat/userIntent/userIntentRecognizer' +export { EditorContextExtractor } from './editor/context/extractor' +export { ChatSessionStorage } from './storages/chatSession' +export { TriggerEventsStorage } from './storages/triggerEvents' +export { ReferenceLogController } from './view/messages/referenceLogController' +export { extractLanguageNameFromFile } from './editor/context/file/languages' diff --git a/packages/core/src/codewhispererChat/storages/triggerEvents.ts b/packages/core/src/codewhispererChat/storages/triggerEvents.ts index 2218d4a11a9..d30ebf48939 100644 --- a/packages/core/src/codewhispererChat/storages/triggerEvents.ts +++ b/packages/core/src/codewhispererChat/storages/triggerEvents.ts @@ -12,6 +12,7 @@ export type TriggerEventType = | 'follow_up' | 'onboarding_page_interaction' | 'quick_action' + | 'inline_chat' export interface TriggerEvent { readonly id: string diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index 3ae076bf53e..1108f0db97a 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -21,7 +21,7 @@ export { Commands } from './vscode/commands2' export { getMachineId } from './vscode/env' export { getLogger } from './logger/logger' export { activateExtension } from './utilities/vsCodeUtils' -export { waitUntil, sleep } from './utilities/timeoutUtils' +export { waitUntil, sleep, Timeout } from './utilities/timeoutUtils' export { Prompter } from './ui/prompter' export { VirtualFileSystem } from './virtualFilesystem' export { VirtualMemoryFile } from './virtualMemoryFile' @@ -55,3 +55,5 @@ export * from './handleUninstall' export { CrashMonitoring } from './crashMonitoring' export { amazonQDiffScheme } from './constants' export * from './featureConfig' +export * from './icons' +export * as textDocumentUtil from './utilities/textDocumentUtilities' diff --git a/packages/core/src/shared/utilities/textDocumentUtilities.ts b/packages/core/src/shared/utilities/textDocumentUtilities.ts index 5a47e231b80..e164523f9bc 100644 --- a/packages/core/src/shared/utilities/textDocumentUtilities.ts +++ b/packages/core/src/shared/utilities/textDocumentUtilities.ts @@ -7,8 +7,10 @@ import * as _path from 'path' import * as vscode from 'vscode' import { getTabSizeSetting } from './editorUtilities' import { tempDirPath } from '../filesystemUtilities' -import { fs, indent, ToolkitError } from '../index' import { getLogger } from '../logger' +import fs from '../fs/fs' +import { ToolkitError } from '../errors' +import { indent } from './textUtilities' /** * Finds occurences of text in a document. Currently only used for highlighting cloudwatchlogs data. @@ -190,3 +192,34 @@ export async function showFile(uri: vscode.Uri) { await vscode.window.showTextDocument(doc, { preview: false }) await vscode.languages.setTextDocumentLanguage(doc, 'log') } + +/** + * Expands the given selection to full line(s) in the document. + * If the selection is partial, it will be extended to include the entire line(s). + * @param document The current text document + * @param selection The current selection + * @returns A new Range that covers full line(s) of the selection + */ +export function expandSelectionToFullLines(document: vscode.TextDocument, selection: vscode.Selection): vscode.Range { + const startLine = document.lineAt(selection.start.line) + const endLine = document.lineAt(selection.end.line) + return new vscode.Range(startLine.range.start, endLine.range.end) +} + +/** + * Ensures the document ends with a newline character. + * If the selection is at the end of the last line and the document doesn't end with a newline, + * this function inserts one. + * @param editor The VS Code text editor to modify + */ +export async function addEofNewline(editor: vscode.TextEditor) { + if ( + editor.selection.end.line === editor.document.lineCount - 1 && + editor.selection.end.character === editor.document.lineAt(editor.selection.end.line).text.length && + !editor.document.getText().endsWith('\n') + ) { + await editor.edit((editBuilder) => { + editBuilder.insert(editor.selection.end, '\n') + }) + } +} diff --git a/packages/core/src/shared/vscode/setContext.ts b/packages/core/src/shared/vscode/setContext.ts index e253666b867..93ddae7b78c 100644 --- a/packages/core/src/shared/vscode/setContext.ts +++ b/packages/core/src/shared/vscode/setContext.ts @@ -33,6 +33,7 @@ export type contextKey = | 'gumby.reviewState' | 'gumby.transformationProposalReviewInProgress' | 'gumby.wasQCodeTransformationUsed' + | 'amazonq.inline.codelensShortcutEnabled' /** * Calls the vscode "setContext" command. diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 25bebbbaa97..884c2f892f0 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -3663,348 +3663,355 @@ "fontCharacter": "\\f1aa" } }, - "aws-amazonq-q-squid-ink": { + "aws-amazonq-q-inline-close": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ab" } }, - "aws-amazonq-q-white": { + "aws-amazonq-q-inline-close-inverse": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ac" } }, - "aws-amazonq-transform-arrow-dark": { + "aws-amazonq-q-squid-ink": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ad" } }, - "aws-amazonq-transform-arrow-light": { + "aws-amazonq-q-white": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ae" } }, - "aws-amazonq-transform-default-dark": { + "aws-amazonq-transform-arrow-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1af" } }, - "aws-amazonq-transform-default-light": { + "aws-amazonq-transform-arrow-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b0" } }, - "aws-amazonq-transform-dependencies-dark": { + "aws-amazonq-transform-default-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b1" } }, - "aws-amazonq-transform-dependencies-light": { + "aws-amazonq-transform-default-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b2" } }, - "aws-amazonq-transform-file-dark": { + "aws-amazonq-transform-dependencies-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b3" } }, - "aws-amazonq-transform-file-light": { + "aws-amazonq-transform-dependencies-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b4" } }, - "aws-amazonq-transform-landing-page-icon": { + "aws-amazonq-transform-file-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b5" } }, - "aws-amazonq-transform-logo": { + "aws-amazonq-transform-file-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b6" } }, - "aws-amazonq-transform-step-into-dark": { + "aws-amazonq-transform-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b7" } }, - "aws-amazonq-transform-step-into-light": { + "aws-amazonq-transform-step-into-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b8" } }, - "aws-amazonq-transform-variables-dark": { + "aws-amazonq-transform-step-into-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1b9" } }, - "aws-amazonq-transform-variables-light": { + "aws-amazonq-transform-variables-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ba" } }, - "aws-applicationcomposer-icon": { + "aws-amazonq-transform-variables-light": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bb" } }, - "aws-applicationcomposer-icon-dark": { + "aws-applicationcomposer-icon": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bc" } }, - "aws-apprunner-service": { + "aws-applicationcomposer-icon-dark": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bd" } }, - "aws-cdk-logo": { + "aws-apprunner-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1be" } }, - "aws-cloudformation-stack": { + "aws-cdk-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1bf" } }, - "aws-cloudwatch-log-group": { + "aws-cloudformation-stack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c0" } }, - "aws-codecatalyst-logo": { + "aws-cloudwatch-log-group": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c1" } }, - "aws-codewhisperer-icon-black": { + "aws-codecatalyst-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c2" } }, - "aws-codewhisperer-icon-white": { + "aws-codewhisperer-icon-black": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c3" } }, - "aws-codewhisperer-learn": { + "aws-codewhisperer-icon-white": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c4" } }, - "aws-ecr-registry": { + "aws-codewhisperer-learn": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c5" } }, - "aws-ecs-cluster": { + "aws-ecr-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c6" } }, - "aws-ecs-container": { + "aws-ecs-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c7" } }, - "aws-ecs-service": { + "aws-ecs-container": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c8" } }, - "aws-generic-attach-file": { + "aws-ecs-service": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1c9" } }, - "aws-iot-certificate": { + "aws-generic-attach-file": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ca" } }, - "aws-iot-policy": { + "aws-iot-certificate": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cb" } }, - "aws-iot-thing": { + "aws-iot-policy": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cc" } }, - "aws-lambda-function": { + "aws-iot-thing": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cd" } }, - "aws-mynah-MynahIconBlack": { + "aws-lambda-function": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1ce" } }, - "aws-mynah-MynahIconWhite": { + "aws-mynah-MynahIconBlack": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1cf" } }, - "aws-mynah-logo": { + "aws-mynah-MynahIconWhite": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d0" } }, - "aws-redshift-cluster": { + "aws-mynah-logo": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d1" } }, - "aws-redshift-cluster-connected": { + "aws-redshift-cluster": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d2" } }, - "aws-redshift-database": { + "aws-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d3" } }, - "aws-redshift-redshift-cluster-connected": { + "aws-redshift-database": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d4" } }, - "aws-redshift-schema": { + "aws-redshift-redshift-cluster-connected": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d5" } }, - "aws-redshift-table": { + "aws-redshift-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d6" } }, - "aws-s3-bucket": { + "aws-redshift-table": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d7" } }, - "aws-s3-create-bucket": { + "aws-s3-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d8" } }, - "aws-schemas-registry": { + "aws-s3-create-bucket": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1d9" } }, - "aws-schemas-schema": { + "aws-schemas-registry": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1da" } }, - "aws-stepfunctions-preview": { + "aws-schemas-schema": { "description": "AWS Contributed Icon", "default": { "fontPath": "./resources/fonts/aws-toolkit-icons.woff", "fontCharacter": "\\f1db" } + }, + "aws-stepfunctions-preview": { + "description": "AWS Contributed Icon", + "default": { + "fontPath": "./resources/fonts/aws-toolkit-icons.woff", + "fontCharacter": "\\f1dc" + } } }, "notebooks": [