From c79ee81bfb7611b93cd4cac54086938926fb524a Mon Sep 17 00:00:00 2001 From: Grant Gurvis Date: Tue, 22 Oct 2024 15:52:25 -0700 Subject: [PATCH 1/7] feat(amazonq): Add inline chat --- package-lock.json | 9 +- package.json | 1 + packages/amazonq/package.json | 134 +++--- packages/amazonq/src/app/chat/activation.ts | 6 +- packages/amazonq/src/inlineChat/app.ts | 12 + .../codeLenses/codeLenseProvider.ts | 62 +++ .../command/registerInlineCommands.ts | 22 + .../controller/inlineChatController.ts | 434 ++++++++++++++++++ .../src/inlineChat/controller/inlineTask.ts | 150 ++++++ .../src/inlineChat/controller/utils.ts | 35 ++ .../decorations/computeDecorations.ts | 32 ++ .../inlineChat/decorations/inlineDecorator.ts | 38 ++ .../inlineLineAnnotationController.ts | 57 +++ .../src/inlineChat/output/computeDiff.ts | 116 +++++ .../inlineChat/output/responseTransformer.ts | 41 ++ .../inlineChat/provider/inlineChatProvider.ts | 209 +++++++++ packages/core/src/amazonq/index.ts | 2 + .../src/codewhisperer/client/codewhisperer.ts | 1 + .../codewhisperer/client/user-service-2.json | 320 +++++++++++-- packages/core/src/codewhisperer/index.ts | 3 + .../views/lineAnnotationController.ts | 43 +- .../controllers/chat/chatRequest/converter.ts | 5 +- .../controllers/chat/model.ts | 1 + packages/core/src/codewhispererChat/index.ts | 8 + .../storages/triggerEvents.ts | 1 + packages/core/src/shared/index.ts | 3 +- packages/core/src/shared/vscode/setContext.ts | 1 + packages/toolkit/package.json | 105 +++-- 28 files changed, 1713 insertions(+), 138 deletions(-) create mode 100644 packages/amazonq/src/inlineChat/app.ts create mode 100644 packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts create mode 100644 packages/amazonq/src/inlineChat/command/registerInlineCommands.ts create mode 100644 packages/amazonq/src/inlineChat/controller/inlineChatController.ts create mode 100644 packages/amazonq/src/inlineChat/controller/inlineTask.ts create mode 100644 packages/amazonq/src/inlineChat/controller/utils.ts create mode 100644 packages/amazonq/src/inlineChat/decorations/computeDecorations.ts create mode 100644 packages/amazonq/src/inlineChat/decorations/inlineDecorator.ts create mode 100644 packages/amazonq/src/inlineChat/decorations/inlineLineAnnotationController.ts create mode 100644 packages/amazonq/src/inlineChat/output/computeDiff.ts create mode 100644 packages/amazonq/src/inlineChat/output/responseTransformer.ts create mode 100644 packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts 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/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..249c8c9f4fb --- /dev/null +++ b/packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts @@ -0,0 +1,62 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +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: + this.codeLenses = [] + this.codeLenses.push( + new vscode.CodeLens(new vscode.Range(task.selectedRange.start, task.selectedRange.start), { + title: 'Accept ($(newline))', + command: 'aws.amazonq.inline.waitForUserDecisionAcceptAll', + arguments: [task], + }) + ) + this.codeLenses.push( + new vscode.CodeLens(new vscode.Range(task.selectedRange.start, task.selectedRange.start), { + title: `Reject ( \u238B )`, + 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..9c0ac363c70 --- /dev/null +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -0,0 +1,434 @@ +/*! + * 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 } from 'aws-core-vscode/shared' +import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' +import { fixEofNewline } from './utils' + +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 fixEofNewline(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 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 range = task.newSelectedRange ?? task.selectedRange + // const newRange = new vscode.Range( + // range!.start, + // new vscode.Position(range.start.line + task.estimatedResponse!.split('\n').length - 1, 0) + // ) + // task.newSelectedRange = newRange + // if (visibleEditor) { + // await visibleEditor.edit( + // (editBuilder) => { + // editBuilder.replace(range, task.estimatedResponse!) + // }, + // 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..10ca03282b7 --- /dev/null +++ b/packages/amazonq/src/inlineChat/controller/inlineTask.ts @@ -0,0 +1,150 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { Decorations } from '../decorations/inlineDecorator' +import { CodeReference } from 'aws-core-vscode/amazonq' +import { computeDecorations } from '../decorations/computeDecorations' +import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' +import { expandSelectionToFullLines } from './utils' + +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 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 = expandSelectionToFullLines(document, selection) + this.selectedText = document.getText(this.selectedRange) + } + + 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.start.line - diff.range.end.line + 1 + } else { + numSuggestionDelChars += diff.originalText.length + numSuggestionDelLines += diff.range.start.line - diff.range.end.line + 1 + } + } + + 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, + } + return event + } + + public isActiveState() { + return !(this.state === TaskState.Complete || this.state === TaskState.Error) + } +} diff --git a/packages/amazonq/src/inlineChat/controller/utils.ts b/packages/amazonq/src/inlineChat/controller/utils.ts new file mode 100644 index 00000000000..44af4ad7f66 --- /dev/null +++ b/packages/amazonq/src/inlineChat/controller/utils.ts @@ -0,0 +1,35 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' + +/** + * 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) +} + +/** + * Fixes the end-of-file newline for the given editor. If the selection is at the end of the + * last line of the document, this function Iinserts a newline character. + * @param editor The VS Code text editor to fix + */ +export async function fixEofNewline(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/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..9cbbe6a56fe --- /dev/null +++ b/packages/amazonq/src/inlineChat/output/responseTransformer.ts @@ -0,0 +1,41 @@ +/*! + * 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' + +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) { + if (err instanceof Error) { + getLogger().error(err) + } else { + getLogger().error(`An unknown error occurred: ${err}`) + } + return undefined + } +} + +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..99c3f3840ec --- /dev/null +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -0,0 +1,209 @@ +/*! + * 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, FeatureAuthState, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { + ChatSessionStorage, + ChatTriggerType, + EditorContextExtractor, + PromptMessage, + TriggerEventsStorage, + TriggerPayload, + triggerPayloadToChatRequest, + UserIntentRecognizer, +} from 'aws-core-vscode/codewhispererChat' +import { AwsClientResponseError, getLogger, isAwsError } from 'aws-core-vscode/shared' +import { randomUUID } from 'crypto' +import { AuthFollowUpType, AuthMessageDataMap } from 'aws-core-vscode/amazonq' +import { codeWhispererClient } from 'aws-core-vscode/codewhisperer' +import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' +import { InlineTask } from '../controller/inlineTask' + +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') + ) { + await this.sendAuthNeededExceptionMessage(credentialsState, tabID, triggerID) + return + } + 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) + this.errorEmitter.fire() + return undefined + } + + return response + } + + private processException(e: any, tabID: string) { + let errorMessage = '' + let requestID = undefined + const defaultMessage = 'Failed to get response' + 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) ?? defaultMessage + } else if (e instanceof CodeWhispererStreamingServiceException) { + errorMessage = e.message + requestID = e.$metadata.requestId + } else if (e instanceof Error) { + errorMessage = e.message + } + + this.errorEmitter.fire() + void vscode.window.showErrorMessage(errorMessage) + getLogger().error(`error: ${errorMessage} tabID: ${tabID} requestID: ${requestID}`) + this.sessionStorage.deleteSession(tabID) + } + + private 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 + } + + this.errorEmitter.fire() + void vscode.window.showErrorMessage(message) + } + + 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..1b20cc479e0 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -28,6 +28,8 @@ 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' import { FeatureContext } from '../shared' /** diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 5104ef7ede0..64bb685e1e3 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -226,6 +226,7 @@ export class DefaultCodeWhispererClient { const client = await this.createUserSdkClient() const requester = async (request: CodeWhispererUserClient.ListAvailableCustomizationsRequest) => client.listAvailableCustomizations(request).promise() + // @ts-ignore return pageableToCollection(requester, {}, 'nextToken') .promise() .then((resps) => { diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 3a94931dddb..94a68ef0f46 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,30 @@ "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" }, + "charactersAdded": { "shape": "PrimitiveInteger" }, + "charactersRemoved": { "shape": "PrimitiveInteger" } + } + }, + "InlineChatUserDecision": { + "type": "string", + "enum": ["ACCEPT", "REJECT", "DISMISS"] + }, "Integer": { "type": "integer", "box": true @@ -1313,6 +1414,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 +1432,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 +1508,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 +1561,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 +1640,8 @@ }, "telemetryEvent": { "shape": "TelemetryEvent" }, "optOutPreference": { "shape": "OptOutPreference" }, - "userContext": { "shape": "UserContext" } + "userContext": { "shape": "UserContext" }, + "profileArn": { "shape": "ProfileArn" } } }, "SendTelemetryEventResponse": { @@ -1506,6 +1652,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 +1764,11 @@ "members": { "artifacts": { "shape": "ArtifactMap" }, "programmingLanguage": { "shape": "ProgrammingLanguage" }, - "scope": { "shape": "CodeAnalysisScope" }, "clientToken": { "shape": "StartCodeAnalysisRequestClientTokenString", "idempotencyToken": true }, + "scope": { "shape": "CodeAnalysisScope" }, "codeScanName": { "shape": "CodeScanName" } } }, @@ -1641,7 +1796,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 +1927,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 +2003,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 +2145,7 @@ }, "TransformationDownloadArtifactType": { "type": "string", - "enum": ["ClientInstructions", "Logs"] + "enum": ["ClientInstructions", "Logs", "GeneratedCode"] }, "TransformationDownloadArtifacts": { "type": "list", @@ -1962,7 +2178,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 +2219,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 +2306,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 +2413,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 +2444,8 @@ "SHOW_EXAMPLES", "CITE_SOURCES", "EXPLAIN_LINE_BY_LINE", - "EXPLAIN_CODE_SELECTION" + "EXPLAIN_CODE_SELECTION", + "GENERATE_CLOUDFORMATION_TEMPLATE" ] }, "UserModificationEvent": { @@ -2217,6 +2470,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/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..f61098a05e7 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,4 @@ export * from './handleUninstall' export { CrashMonitoring } from './crashMonitoring' export { amazonQDiffScheme } from './constants' export * from './featureConfig' +export * from './icons' 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": [ From 563338492e144d53db1fb652b7324d1760b679f4 Mon Sep 17 00:00:00 2001 From: Grant Gurvis Date: Sat, 26 Oct 2024 18:05:17 -0700 Subject: [PATCH 2/7] Address PR comments --- .../inlineChat/output/responseTransformer.ts | 6 +-- .../inlineChat/provider/inlineChatProvider.ts | 51 +++++-------------- packages/core/src/amazonq/index.ts | 1 + packages/core/src/amazonq/util/authUtils.ts | 31 +++++++++++ .../controllers/chat/messenger/messenger.ts | 23 +-------- 5 files changed, 49 insertions(+), 63 deletions(-) create mode 100644 packages/core/src/amazonq/util/authUtils.ts diff --git a/packages/amazonq/src/inlineChat/output/responseTransformer.ts b/packages/amazonq/src/inlineChat/output/responseTransformer.ts index 9cbbe6a56fe..96e5b0436c5 100644 --- a/packages/amazonq/src/inlineChat/output/responseTransformer.ts +++ b/packages/amazonq/src/inlineChat/output/responseTransformer.ts @@ -23,11 +23,7 @@ export function responseTransformer( return decodedResponse } } catch (err) { - if (err instanceof Error) { - getLogger().error(err) - } else { - getLogger().error(`An unknown error occurred: ${err}`) - } + getLogger().error('An unknown error occurred: %s', (err as Error).message) return undefined } } diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index 99c3f3840ec..27211460cae 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -19,12 +19,12 @@ import { triggerPayloadToChatRequest, UserIntentRecognizer, } from 'aws-core-vscode/codewhispererChat' -import { AwsClientResponseError, getLogger, isAwsError } from 'aws-core-vscode/shared' +import { AwsClientResponseError, getLogger, isAwsError, ToolkitError } from 'aws-core-vscode/shared' import { randomUUID } from 'crypto' -import { AuthFollowUpType, AuthMessageDataMap } from 'aws-core-vscode/amazonq' 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 @@ -100,12 +100,12 @@ export class InlineChatProvider { const tabID = triggerEvent.tabID const credentialsState = await AuthUtil.instance.getChatAuthState() - if ( !(credentialsState.codewhispererChat === 'connected' && credentialsState.codewhispererCore === 'connected') ) { - await this.sendAuthNeededExceptionMessage(credentialsState, tabID, triggerID) - return + const { message } = extractAuthFollowUp(credentialsState) + this.errorEmitter.fire() + throw new ToolkitError(message) } triggerPayload.useRelevantDocuments = false @@ -128,22 +128,19 @@ export class InlineChatProvider { ) } catch (e: any) { this.processException(e, tabID) - this.errorEmitter.fire() - return undefined } return response } private processException(e: any, tabID: string) { - let errorMessage = '' - let requestID = undefined - const defaultMessage = 'Failed to get response' + 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) ?? defaultMessage + errorMessage = AwsClientResponseError.tryExtractReasonFromSyntaxError(e) } else if (e instanceof CodeWhispererStreamingServiceException) { errorMessage = e.message requestID = e.$metadata.requestId @@ -152,34 +149,14 @@ export class InlineChatProvider { } this.errorEmitter.fire() - void vscode.window.showErrorMessage(errorMessage) - getLogger().error(`error: ${errorMessage} tabID: ${tabID} requestID: ${requestID}`) this.sessionStorage.deleteSession(tabID) - } - - private 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 - } - - this.errorEmitter.fire() - void vscode.window.showErrorMessage(message) + throw ToolkitError.chain(e, errorMessage ?? 'Failed to get response', { + details: { + tabID, + requestID, + }, + }) } public sendTelemetryEvent(inlineChatEvent: InlineChatEvent, currentTask?: InlineTask) { diff --git a/packages/core/src/amazonq/index.ts b/packages/core/src/amazonq/index.ts index 1b20cc479e0..d015bfb6122 100644 --- a/packages/core/src/amazonq/index.ts +++ b/packages/core/src/amazonq/index.ts @@ -30,6 +30,7 @@ export { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispere 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..587ae05fe45 --- /dev/null +++ b/packages/core/src/amazonq/util/authUtils.ts @@ -0,0 +1,31 @@ +/*! + * 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' + +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/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( { From 6b5ebd414a4f74fbb503ddde03dc6a2923993df5 Mon Sep 17 00:00:00 2001 From: Grant Gurvis Date: Sat, 26 Oct 2024 18:11:44 -0700 Subject: [PATCH 3/7] numSuggestionAddLines/numSuggestionDelLines calculation --- packages/amazonq/src/inlineChat/controller/inlineTask.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amazonq/src/inlineChat/controller/inlineTask.ts b/packages/amazonq/src/inlineChat/controller/inlineTask.ts index 10ca03282b7..2b6ce23b1bc 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineTask.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineTask.ts @@ -121,10 +121,10 @@ export class InlineTask { for (const diff of this.diff) { if (diff.type === 'insertion') { numSuggestionAddChars += diff.replacementText.length - numSuggestionAddLines += diff.range.start.line - diff.range.end.line + 1 + numSuggestionAddLines += diff.range.end.line - diff.range.start.line + 1 } else { numSuggestionDelChars += diff.originalText.length - numSuggestionDelLines += diff.range.start.line - diff.range.end.line + 1 + numSuggestionDelLines += diff.range.end.line - diff.range.start.line + 1 } } From 2dccede42beda19c9f4a63244ff725c304565d20 Mon Sep 17 00:00:00 2001 From: Grant Gurvis Date: Mon, 28 Oct 2024 12:18:13 -0700 Subject: [PATCH 4/7] Updated inline chat event fields --- .../src/inlineChat/controller/inlineTask.ts | 16 +++++++++++++--- .../inlineChat/provider/inlineChatProvider.ts | 2 +- .../src/codewhisperer/client/user-service-2.json | 3 +-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/amazonq/src/inlineChat/controller/inlineTask.ts b/packages/amazonq/src/inlineChat/controller/inlineTask.ts index 2b6ce23b1bc..4d321633470 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineTask.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineTask.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode' -import { Decorations } from '../decorations/inlineDecorator' -import { CodeReference } from 'aws-core-vscode/amazonq' -import { computeDecorations } from '../decorations/computeDecorations' +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 { expandSelectionToFullLines } from './utils' +import { extractLanguageNameFromFile } from 'aws-core-vscode/codewhispererChat' interface TextToInsert { type: 'insertion' @@ -44,6 +45,7 @@ export class InlineTask { public diffBlock: DiffBlock[] = [] public codeReferences: CodeReference[] = [] public selectedText: string + public languageName: string | undefined public partialSelectedText: string | undefined public partialSelectedTextRight: string | undefined @@ -65,6 +67,7 @@ export class InlineTask { ) { this.selectedRange = expandSelectionToFullLines(document, selection) this.selectedText = document.getText(this.selectedRange) + this.languageName = extractLanguageNameFromFile(document) } public revertDiff(): void { @@ -128,6 +131,12 @@ export class InlineTask { } } + const programmingLanguage = this.languageName + ? { + languageName: this.languageName, + } + : undefined + const event: Partial = { requestId: this.requestId, timestamp: new Date(), @@ -140,6 +149,7 @@ export class InlineTask { numSuggestionAddLines, numSuggestionDelChars, numSuggestionDelLines, + programmingLanguage, } return event } diff --git a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts index 27211460cae..91fbad54f93 100644 --- a/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts +++ b/packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts @@ -8,7 +8,7 @@ import { CodeWhispererStreamingServiceException, GenerateAssistantResponseCommandOutput, } from '@amzn/codewhisperer-streaming' -import { AuthUtil, FeatureAuthState, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' +import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' import { ChatSessionStorage, ChatTriggerType, diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 94a68ef0f46..9defffe1063 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -1263,8 +1263,7 @@ "userDecision": { "shape": "InlineChatUserDecision" }, "responseStartLatency": { "shape": "Double" }, "responseEndLatency": { "shape": "Double" }, - "charactersAdded": { "shape": "PrimitiveInteger" }, - "charactersRemoved": { "shape": "PrimitiveInteger" } + "programmingLanguage": { "shape": "ProgrammingLanguage" } } }, "InlineChatUserDecision": { From dcc47aa4efcc8a1d58cd88c0b2c17bcdd9dea238 Mon Sep 17 00:00:00 2001 From: Grant Gurvis Date: Mon, 28 Oct 2024 14:12:02 -0700 Subject: [PATCH 5/7] Address comments --- ...-912fe720-0d1b-40a3-8638-f9a4d2948904.json | 4 ++ ...-31cd9ad4-1e6b-4f05-b910-11c5059bd2fa.json | 4 ++ .../codeLenses/codeLenseProvider.ts | 24 +++++++--- .../controller/inlineChatController.ts | 45 +------------------ .../src/inlineChat/controller/inlineTask.ts | 2 +- .../src/inlineChat/controller/utils.ts | 35 --------------- .../inlineChat/output/responseTransformer.ts | 18 ++++++++ packages/core/src/amazonq/util/authUtils.ts | 9 ++++ .../src/codewhisperer/client/codewhisperer.ts | 1 - packages/core/src/shared/index.ts | 1 + .../shared/utilities/textDocumentUtilities.ts | 31 +++++++++++++ 11 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 packages/amazonq/.changes/next-release/Breaking Change-912fe720-0d1b-40a3-8638-f9a4d2948904.json create mode 100644 packages/amazonq/.changes/next-release/Feature-31cd9ad4-1e6b-4f05-b910-11c5059bd2fa.json delete mode 100644 packages/amazonq/src/inlineChat/controller/utils.ts 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/src/inlineChat/codeLenses/codeLenseProvider.ts b/packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts index 249c8c9f4fb..34a85e10b38 100644 --- a/packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts +++ b/packages/amazonq/src/inlineChat/codeLenses/codeLenseProvider.ts @@ -4,6 +4,7 @@ */ import * as vscode from 'vscode' +import * as os from 'os' import { InlineTask, TaskState } from '../controller/inlineTask' export class CodelensProvider implements vscode.CodeLensProvider { @@ -27,7 +28,7 @@ export class CodelensProvider implements vscode.CodeLensProvider { return } switch (task.state) { - case TaskState.InProgress: + case TaskState.InProgress: { this.codeLenses = [] this.codeLenses.push( new vscode.CodeLens(new vscode.Range(task.selectedRange.start, task.selectedRange.start), { @@ -36,26 +37,39 @@ export class CodelensProvider implements vscode.CodeLensProvider { }) ) break - case TaskState.WaitingForDecision: + } + 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: 'Accept ($(newline))', + 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: `Reject ( \u238B )`, + title: rejectTitle, command: 'aws.amazonq.inline.waitForUserDecisionRejectAll', arguments: [task], }) ) break - default: + } + default: { this.codeLenses = [] break + } } this._onDidChangeCodeLenses.fire() } diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index 9c0ac363c70..cb0e42724cf 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -13,9 +13,8 @@ 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 } from 'aws-core-vscode/shared' +import { addEofNewline, codicon, getIcon, getLogger, messages, setContext, Timeout } from 'aws-core-vscode/shared' import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' -import { fixEofNewline } from './utils' export class InlineChatController { private task: InlineTask | undefined @@ -181,7 +180,7 @@ export class InlineChatController { if (!query) { return } - await fixEofNewline(editor) + await 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) => { @@ -357,46 +356,6 @@ export class InlineChatController { } } - // 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 range = task.newSelectedRange ?? task.selectedRange - // const newRange = new vscode.Range( - // range!.start, - // new vscode.Position(range.start.line + task.estimatedResponse!.split('\n').length - 1, 0) - // ) - // task.newSelectedRange = newRange - // if (visibleEditor) { - // await visibleEditor.edit( - // (editBuilder) => { - // editBuilder.replace(range, task.estimatedResponse!) - // }, - // 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 diff --git a/packages/amazonq/src/inlineChat/controller/inlineTask.ts b/packages/amazonq/src/inlineChat/controller/inlineTask.ts index 4d321633470..48968ae1baa 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineTask.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineTask.ts @@ -7,8 +7,8 @@ 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 { expandSelectionToFullLines } from './utils' import { extractLanguageNameFromFile } from 'aws-core-vscode/codewhispererChat' +import { expandSelectionToFullLines } from 'aws-core-vscode/shared' interface TextToInsert { type: 'insertion' diff --git a/packages/amazonq/src/inlineChat/controller/utils.ts b/packages/amazonq/src/inlineChat/controller/utils.ts deleted file mode 100644 index 44af4ad7f66..00000000000 --- a/packages/amazonq/src/inlineChat/controller/utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import * as vscode from 'vscode' - -/** - * 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) -} - -/** - * Fixes the end-of-file newline for the given editor. If the selection is at the end of the - * last line of the document, this function Iinserts a newline character. - * @param editor The VS Code text editor to fix - */ -export async function fixEofNewline(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/amazonq/src/inlineChat/output/responseTransformer.ts b/packages/amazonq/src/inlineChat/output/responseTransformer.ts index 96e5b0436c5..b965ced519e 100644 --- a/packages/amazonq/src/inlineChat/output/responseTransformer.ts +++ b/packages/amazonq/src/inlineChat/output/responseTransformer.ts @@ -7,6 +7,14 @@ 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, @@ -28,6 +36,16 @@ export function responseTransformer( } } +/** + * 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') diff --git a/packages/core/src/amazonq/util/authUtils.ts b/packages/core/src/amazonq/util/authUtils.ts index 587ae05fe45..0fd48ebc4c9 100644 --- a/packages/core/src/amazonq/util/authUtils.ts +++ b/packages/core/src/amazonq/util/authUtils.ts @@ -6,6 +6,15 @@ 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 diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 64bb685e1e3..5104ef7ede0 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -226,7 +226,6 @@ export class DefaultCodeWhispererClient { const client = await this.createUserSdkClient() const requester = async (request: CodeWhispererUserClient.ListAvailableCustomizationsRequest) => client.listAvailableCustomizations(request).promise() - // @ts-ignore return pageableToCollection(requester, {}, 'nextToken') .promise() .then((resps) => { diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index f61098a05e7..b1750ead367 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -56,3 +56,4 @@ export { CrashMonitoring } from './crashMonitoring' export { amazonQDiffScheme } from './constants' export * from './featureConfig' export * from './icons' +export * from './utilities/textDocumentUtilities' diff --git a/packages/core/src/shared/utilities/textDocumentUtilities.ts b/packages/core/src/shared/utilities/textDocumentUtilities.ts index 5a47e231b80..4e5e22045f5 100644 --- a/packages/core/src/shared/utilities/textDocumentUtilities.ts +++ b/packages/core/src/shared/utilities/textDocumentUtilities.ts @@ -190,3 +190,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') + }) + } +} From e9790e7fa4dd1a967b7d857fcb81943d3aa3221f Mon Sep 17 00:00:00 2001 From: Grant Gurvis Date: Mon, 28 Oct 2024 14:23:59 -0700 Subject: [PATCH 6/7] Fix circular dependency --- packages/core/src/shared/utilities/textDocumentUtilities.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/shared/utilities/textDocumentUtilities.ts b/packages/core/src/shared/utilities/textDocumentUtilities.ts index 4e5e22045f5..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. From fb0db4767a41e8cfcd417d26067b4319b25aadeb Mon Sep 17 00:00:00 2001 From: Grant Gurvis Date: Mon, 28 Oct 2024 14:31:37 -0700 Subject: [PATCH 7/7] Add named export for textDocumentUtil --- .../amazonq/src/inlineChat/controller/inlineChatController.ts | 4 ++-- packages/amazonq/src/inlineChat/controller/inlineTask.ts | 4 ++-- packages/core/src/shared/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts index cb0e42724cf..bb9cf4b92e0 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineChatController.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineChatController.ts @@ -13,7 +13,7 @@ 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 { addEofNewline, codicon, getIcon, getLogger, messages, setContext, Timeout } from 'aws-core-vscode/shared' +import { codicon, getIcon, getLogger, messages, setContext, Timeout, textDocumentUtil } from 'aws-core-vscode/shared' import { InlineLineAnnotationController } from '../decorations/inlineLineAnnotationController' export class InlineChatController { @@ -180,7 +180,7 @@ export class InlineChatController { if (!query) { return } - await addEofNewline(editor) + 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) => { diff --git a/packages/amazonq/src/inlineChat/controller/inlineTask.ts b/packages/amazonq/src/inlineChat/controller/inlineTask.ts index 48968ae1baa..a6a169ad58c 100644 --- a/packages/amazonq/src/inlineChat/controller/inlineTask.ts +++ b/packages/amazonq/src/inlineChat/controller/inlineTask.ts @@ -8,7 +8,7 @@ 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 { expandSelectionToFullLines } from 'aws-core-vscode/shared' +import { textDocumentUtil } from 'aws-core-vscode/shared' interface TextToInsert { type: 'insertion' @@ -65,7 +65,7 @@ export class InlineTask { public document: vscode.TextDocument, selection: vscode.Selection ) { - this.selectedRange = expandSelectionToFullLines(document, selection) + this.selectedRange = textDocumentUtil.expandSelectionToFullLines(document, selection) this.selectedText = document.getText(this.selectedRange) this.languageName = extractLanguageNameFromFile(document) } diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index b1750ead367..1108f0db97a 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -56,4 +56,4 @@ export { CrashMonitoring } from './crashMonitoring' export { amazonQDiffScheme } from './constants' export * from './featureConfig' export * from './icons' -export * from './utilities/textDocumentUtilities' +export * as textDocumentUtil from './utilities/textDocumentUtilities'