diff --git a/packages/amazonq/.changes/next-release/Bug Fix-11625969-6700-4d10-805f-b6eaea2287a2.json b/packages/amazonq/.changes/next-release/Bug Fix-11625969-6700-4d10-805f-b6eaea2287a2.json new file mode 100644 index 00000000000..94fcb15d042 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-11625969-6700-4d10-805f-b6eaea2287a2.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q /doc: Prevent users from requesting changes if no iterations remain" +} diff --git a/packages/amazonq/.changes/next-release/Bug Fix-fdf7f991-1da6-405d-8a8a-3ce1af1db29d.json b/packages/amazonq/.changes/next-release/Bug Fix-fdf7f991-1da6-405d-8a8a-3ce1af1db29d.json new file mode 100644 index 00000000000..d4063ad2b7e --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-fdf7f991-1da6-405d-8a8a-3ce1af1db29d.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q /doc: Ask for user prompt if error occurs while updating documentation" +} diff --git a/packages/amazonq/test/e2e/amazonq/doc.test.ts b/packages/amazonq/test/e2e/amazonq/doc.test.ts index ad6b3df914c..343d228c261 100644 --- a/packages/amazonq/test/e2e/amazonq/doc.test.ts +++ b/packages/amazonq/test/e2e/amazonq/doc.test.ts @@ -146,5 +146,33 @@ describe('Amazon Q Doc', async function () { FollowUpTypes.RejectChanges, ]) }) + + it('Handle unrelated prompt error', async () => { + await tab.waitForButtons([FollowUpTypes.UpdateDocumentation]) + + tab.clickButton(FollowUpTypes.UpdateDocumentation) + + await tab.waitForButtons([FollowUpTypes.SynchronizeDocumentation, FollowUpTypes.EditDocumentation]) + + tab.clickButton(FollowUpTypes.EditDocumentation) + + await tab.waitForButtons([FollowUpTypes.ProceedFolderSelection]) + + tab.clickButton(FollowUpTypes.ProceedFolderSelection) + + tab.addChatMessage({ prompt: 'tell me about the weather' }) + + await tab.waitForEvent(() => + tab.getChatItems().some(({ body }) => body?.startsWith(i18n('AWS.amazonq.doc.error.promptUnrelated'))) + ) + + await tab.waitForEvent(() => { + const store = tab.getStore() + return ( + !store.promptInputDisabledState && + store.promptInputPlaceholder === i18n('AWS.amazonq.doc.placeholder.editReadme') + ) + }) + }) }) }) diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 833ed7aad7f..aadddfe68c9 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -375,6 +375,7 @@ "AWS.amazonq.doc.answer.readmeCreated": "I've created a README for your code.", "AWS.amazonq.doc.answer.readmeUpdated": "I've updated your README.", "AWS.amazonq.doc.answer.codeResult": "You can accept the changes to your files, or describe any additional changes you'd like me to make.", + "AWS.amazonq.doc.answer.acceptOrReject": "You can accept or reject the changes to your files.", "AWS.amazonq.doc.answer.scanning": "Scanning source files", "AWS.amazonq.doc.answer.summarizing": "Summarizing source files", "AWS.amazonq.doc.answer.generating": "Generating documentation", @@ -384,7 +385,7 @@ "AWS.amazonq.doc.error.noFolderSelected": "It looks like you didn't choose a folder. Choose a folder to continue.", "AWS.amazonq.doc.error.contentLengthError": "Your workspace is too large for me to review. Your workspace must be within the quota, even if you choose a smaller folder. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.readmeTooLarge": "The README in your folder is too large for me to review. Try reducing the size of your README, or choose a folder with a smaller README. For more information on quotas, see the Amazon Q Developer documentation.", - "AWS.amazonq.doc.error.readmeUpdateTooLarge": "The updated README is too large. Try reducing the size of your README, or asking for a smaller update. For more information on quotas, see the Amazon Q Developer documentation.", + "AWS.amazonq.doc.error.readmeUpdateTooLarge": "The updated README exceeds document size limits. Try reducing the size of your current README or working on a smaller task that won't produce as much content. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.workspaceEmpty": "The folder you chose did not contain any source files in a supported language. Choose another folder and try again. For more information on supported languages, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.promptTooVague": "I need more information to make changes to your README. Try providing some of the following details:\n- Which sections you want to modify\n- The content you want to add or remove\n- Specific issues that need correcting\n\nFor more information on prompt best practices, see the Amazon Q Developer documentation.", "AWS.amazonq.doc.error.promptUnrelated": "These changes don't seem related to documentation. Try describing your changes again, using the following best practices:\n- Changes should relate to how project functionality is reflected in the README\n- Content you refer to should be available in your codebase\n\n For more information on prompt best practices, see the Amazon Q Developer documentation.", @@ -396,6 +397,9 @@ "AWS.amazonq.doc.pillText.newTask": "Start a new documentation task", "AWS.amazonq.doc.pillText.update": "Update README to reflect code", "AWS.amazonq.doc.pillText.makeChange": "Make a specific change", + "AWS.amazonq.doc.pillText.accept": "Accept", + "AWS.amazonq.doc.pillText.reject": "Reject", + "AWS.amazonq.doc.pillText.makeChanges": "Make changes", "AWS.amazonq.inline.invokeChat": "Inline chat", "AWS.toolkit.lambda.walkthrough.quickpickTitle": "Application Builder Walkthrough", "AWS.toolkit.lambda.walkthrough.title": "Get started building your application", diff --git a/packages/core/src/amazonq/commons/connector/baseMessenger.ts b/packages/core/src/amazonq/commons/connector/baseMessenger.ts index ab053333432..4c29f005557 100644 --- a/packages/core/src/amazonq/commons/connector/baseMessenger.ts +++ b/packages/core/src/amazonq/commons/connector/baseMessenger.ts @@ -85,6 +85,7 @@ export class Messenger { type: 'answer', tabID: tabID, message: i18n('AWS.amazonq.featureDev.error.monthlyLimitReached'), + disableChatInput: true, }) this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.featureDev.placeholder.chatInputDisabled')) } diff --git a/packages/core/src/amazonqDoc/constants.ts b/packages/core/src/amazonqDoc/constants.ts index 5d1d938c940..90284a90648 100644 --- a/packages/core/src/amazonqDoc/constants.ts +++ b/packages/core/src/amazonqDoc/constants.ts @@ -92,6 +92,43 @@ export const FolderSelectorFollowUps = [ }, ] +export const CodeChangeFollowUps = [ + { + pillText: i18n('AWS.amazonq.doc.pillText.accept'), + prompt: i18n('AWS.amazonq.doc.pillText.accept'), + type: FollowUpTypes.AcceptChanges, + icon: 'ok' as MynahIcons, + status: 'success' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.makeChanges'), + prompt: i18n('AWS.amazonq.doc.pillText.makeChanges'), + type: FollowUpTypes.MakeChanges, + icon: 'refresh' as MynahIcons, + status: 'info' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.reject'), + prompt: i18n('AWS.amazonq.doc.pillText.reject'), + type: FollowUpTypes.RejectChanges, + icon: 'cancel' as MynahIcons, + status: 'error' as Status, + }, +] + +export const NewSessionFollowUps = [ + { + pillText: i18n('AWS.amazonq.doc.pillText.newTask'), + type: FollowUpTypes.NewTask, + status: 'info' as Status, + }, + { + pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), + type: FollowUpTypes.CloseSession, + status: 'info' as Status, + }, +] + export const SynchronizeDocumentation = { pillText: i18n('AWS.amazonq.doc.pillText.update'), prompt: i18n('AWS.amazonq.doc.pillText.update'), diff --git a/packages/core/src/amazonqDoc/controllers/chat/controller.ts b/packages/core/src/amazonqDoc/controllers/chat/controller.ts index 8246a011fbe..c91a387484a 100644 --- a/packages/core/src/amazonqDoc/controllers/chat/controller.ts +++ b/packages/core/src/amazonqDoc/controllers/chat/controller.ts @@ -10,7 +10,9 @@ import { EditDocumentation, FolderSelectorFollowUps, Mode, + NewSessionFollowUps, SynchronizeDocumentation, + CodeChangeFollowUps, docScheme, featureName, findReadmePath, @@ -22,7 +24,6 @@ import { Session } from '../../session/session' import { i18n } from '../../../shared/i18n-helper' import path from 'path' import { createSingleFileDialog } from '../../../shared/ui/common/openDialog' -import { MynahIcons } from '@aws/mynah-ui' import { MonthlyConversationLimitError, @@ -298,18 +299,7 @@ export class DocController { tabID: data?.tabID, disableChatInput: true, message: 'Your changes have been discarded.', - followUps: [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps: NewSessionFollowUps, }) break case FollowUpTypes.ProceedFolderSelection: @@ -412,13 +402,19 @@ export class DocController { const errorMessage = createUserFacingErrorMessage(`${err.cause?.message ?? err.message}`) // eslint-disable-next-line unicorn/no-null this.messenger.sendUpdatePromptProgress(message.tabID, null) + if (err.constructor.name === MonthlyConversationLimitError.name) { + this.messenger.sendMonthlyLimitError(message.tabID) + } else { + const enableUserInput = this.mode === Mode.EDIT && err.remainingIterations > 0 - switch (err.constructor.name) { - case MonthlyConversationLimitError.name: - this.messenger.sendMonthlyLimitError(message.tabID) - break - default: - this.messenger.sendErrorMessage(errorMessage, message.tabID, 0, session?.conversationIdUnsafe, false) + this.messenger.sendErrorMessage( + errorMessage, + message.tabID, + 0, + session?.conversationIdUnsafe, + false, + enableUserInput + ) } } @@ -427,8 +423,6 @@ export class DocController { await this.onDocsGeneration(session, message.message, message.tabID) } catch (err: any) { this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) } } @@ -461,12 +455,8 @@ export class DocController { } await this.generateDocumentation({ message, session }) - this.messenger.sendChatInputEnabled(message?.tabID, false) - this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) } catch (err: any) { this.processErrorChatMessage(err, message, session) - // Lock the chat input until they explicitly click one of the follow ups - this.messenger.sendChatInputEnabled(message.tabID, false) } } @@ -590,40 +580,21 @@ export class DocController { this.messenger.sendAnswer({ type: 'answer', tabID: tabID, - message: `${this.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${i18n('AWS.amazonq.doc.answer.codeResult')}`, + message: `${this.mode === Mode.CREATE ? i18n('AWS.amazonq.doc.answer.readmeCreated') : i18n('AWS.amazonq.doc.answer.readmeUpdated')} ${remainingIterations > 0 ? i18n('AWS.amazonq.doc.answer.codeResult') : i18n('AWS.amazonq.doc.answer.acceptOrReject')}`, disableChatInput: true, }) - } - this.messenger.sendAnswer({ - message: undefined, - type: 'system-prompt', - disableChatInput: true, - followUps: [ - { - pillText: 'Accept', - prompt: 'Accept', - type: FollowUpTypes.AcceptChanges, - icon: 'ok' as MynahIcons, - status: 'success', - }, - { - pillText: 'Make changes', - prompt: 'Make changes', - type: FollowUpTypes.MakeChanges, - icon: 'refresh' as MynahIcons, - status: 'info', - }, - { - pillText: 'Reject', - prompt: 'Reject', - type: FollowUpTypes.RejectChanges, - icon: 'cancel' as MynahIcons, - status: 'error', - }, - ], - tabID: tabID, - }) + this.messenger.sendAnswer({ + message: undefined, + type: 'system-prompt', + disableChatInput: true, + followUps: + remainingIterations > 0 + ? CodeChangeFollowUps + : CodeChangeFollowUps.filter((followUp) => followUp.type !== FollowUpTypes.MakeChanges), + tabID: tabID, + }) + } } finally { if (session?.state?.tokenSource?.token.isCancellationRequested) { await this.newTask({ tabID }) @@ -642,10 +613,8 @@ export class DocController { type: 'answer', tabID: message.tabID, message: 'Follow instructions to re-authenticate ...', + disableChatInput: true, }) - - // Explicitly ensure the user goes through the re-authenticate flow - this.messenger.sendChatInputEnabled(message.tabID, false) } private tabClosed(message: any) { @@ -670,18 +639,7 @@ export class DocController { type: 'answer', disableChatInput: true, tabID: message.tabID, - followUps: [ - { - pillText: i18n('AWS.amazonq.doc.pillText.newTask'), - type: FollowUpTypes.NewTask, - status: 'info', - }, - { - pillText: i18n('AWS.amazonq.doc.pillText.closeSession'), - type: FollowUpTypes.CloseSession, - status: 'info', - }, - ], + followUps: NewSessionFollowUps, }) this.messenger.sendUpdatePlaceholder(message.tabID, i18n('AWS.amazonq.doc.pillText.selectOption')) diff --git a/packages/core/src/amazonqDoc/errors.ts b/packages/core/src/amazonqDoc/errors.ts index d9794f16327..11a6514a616 100644 --- a/packages/core/src/amazonqDoc/errors.ts +++ b/packages/core/src/amazonqDoc/errors.ts @@ -7,69 +7,57 @@ import { ToolkitError } from '../shared/errors' import { i18n } from '../shared/i18n-helper' export class DocServiceError extends ToolkitError { - constructor(message: string, code: string) { + remainingIterations?: number + constructor(message: string, code: string, remainingIterations?: number) { super(message, { code }) + this.remainingIterations = remainingIterations } } -export class ReadmeTooLargeError extends ToolkitError { +export class ReadmeTooLargeError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), { - code: ReadmeTooLargeError.name, - }) + super(i18n('AWS.amazonq.doc.error.readmeTooLarge'), ReadmeTooLargeError.name) } } -export class ReadmeUpdateTooLargeError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), { - code: ReadmeUpdateTooLargeError.name, - }) +export class ReadmeUpdateTooLargeError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.readmeUpdateTooLarge'), ReadmeUpdateTooLargeError.name, remainingIterations) } } -export class WorkspaceEmptyError extends ToolkitError { +export class WorkspaceEmptyError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), { - code: WorkspaceEmptyError.name, - }) + super(i18n('AWS.amazonq.doc.error.workspaceEmpty'), WorkspaceEmptyError.name) } } -export class NoChangeRequiredException extends ToolkitError { +export class NoChangeRequiredException extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), { - code: NoChangeRequiredException.name, - }) + super(i18n('AWS.amazonq.doc.error.noChangeRequiredException'), NoChangeRequiredException.name) } } -export class PromptRefusalException extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptRefusal'), { - code: PromptRefusalException.name, - }) +export class PromptRefusalException extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptRefusal'), PromptRefusalException.name, remainingIterations) } } -export class ContentLengthError extends ToolkitError { +export class ContentLengthError extends DocServiceError { constructor() { - super(i18n('AWS.amazonq.doc.error.contentLengthError'), { code: ContentLengthError.name }) + super(i18n('AWS.amazonq.doc.error.contentLengthError'), ContentLengthError.name) } } -export class PromptTooVagueError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptTooVague'), { - code: PromptTooVagueError.name, - }) +export class PromptTooVagueError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptTooVague'), PromptTooVagueError.name, remainingIterations) } } -export class PromptUnrelatedError extends ToolkitError { - constructor() { - super(i18n('AWS.amazonq.doc.error.promptUnrelated'), { - code: PromptUnrelatedError.name, - }) +export class PromptUnrelatedError extends DocServiceError { + constructor(remainingIterations: number) { + super(i18n('AWS.amazonq.doc.error.promptUnrelated'), PromptUnrelatedError.name, remainingIterations) } } diff --git a/packages/core/src/amazonqDoc/messenger.ts b/packages/core/src/amazonqDoc/messenger.ts index 09be3dd11fb..f28e5e9060b 100644 --- a/packages/core/src/amazonqDoc/messenger.ts +++ b/packages/core/src/amazonqDoc/messenger.ts @@ -4,10 +4,9 @@ */ import { Messenger } from '../amazonq/commons/connector/baseMessenger' import { AppToWebViewMessageDispatcher } from '../amazonq/commons/connector/connectorMessages' -import { FollowUpTypes } from '../amazonq/commons/types' import { messageWithConversationId } from '../amazonqFeatureDev' import { i18n } from '../shared/i18n-helper' -import { docGenerationProgressMessage, DocGenerationStep, Mode } from './constants' +import { docGenerationProgressMessage, DocGenerationStep, Mode, NewSessionFollowUps } from './constants' import { inProgress } from './types' export class DocMessenger extends Messenger { @@ -48,25 +47,19 @@ export class DocMessenger extends Messenger { tabID: string, _retries: number, conversationId?: string, - _showDefaultMessage?: boolean + _showDefaultMessage?: boolean, + enableUserInput?: boolean ) { + if (enableUserInput) { + this.sendUpdatePlaceholder(tabID, i18n('AWS.amazonq.doc.placeholder.editReadme')) + this.sendChatInputEnabled(tabID, true) + } this.sendAnswer({ type: 'answer', tabID: tabID, message: errorMessage + messageWithConversationId(conversationId), - }) - - this.sendAnswer({ - message: undefined, - type: 'system-prompt', - followUps: [ - { - pillText: i18n('AWS.amazonq.featureDev.pillText.retry'), - type: FollowUpTypes.Retry, - status: 'warning', - }, - ], - tabID, + followUps: enableUserInput ? [] : NewSessionFollowUps, + disableChatInput: !enableUserInput, }) } } diff --git a/packages/core/src/amazonqDoc/session/sessionState.ts b/packages/core/src/amazonqDoc/session/sessionState.ts index b3404c7998a..7bf9c02e51b 100644 --- a/packages/core/src/amazonqDoc/session/sessionState.ts +++ b/packages/core/src/amazonqDoc/session/sessionState.ts @@ -97,7 +97,7 @@ abstract class CodeGenBase { ++pollingIteration ) { const codegenResult = await this.config.proxyClient.getCodeGeneration(this.conversationId, codeGenerationId) - const codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount + const codeGenerationRemainingIterationCount = codegenResult.codeGenerationRemainingIterationCount || 0 const codeGenerationTotalIterationCount = codegenResult.codeGenerationTotalIterationCount getLogger().debug(`Codegen response: %O`, codegenResult) @@ -151,7 +151,7 @@ abstract class CodeGenBase { throw new ReadmeTooLargeError() } case codegenResult.codeGenerationStatusDetail?.includes('README_UPDATE_TOO_LARGE'): { - throw new ReadmeUpdateTooLargeError() + throw new ReadmeUpdateTooLargeError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('WORKSPACE_TOO_LARGE'): { throw new ContentLengthError() @@ -160,18 +160,19 @@ abstract class CodeGenBase { throw new WorkspaceEmptyError() } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_UNRELATED'): { - throw new PromptUnrelatedError() + throw new PromptUnrelatedError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_TOO_VAGUE'): { - throw new PromptTooVagueError() + throw new PromptTooVagueError(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('PROMPT_REFUSAL'): { - throw new PromptRefusalException() + throw new PromptRefusalException(codeGenerationRemainingIterationCount) } case codegenResult.codeGenerationStatusDetail?.includes('Guardrails'): { throw new DocServiceError( i18n('AWS.amazonq.doc.error.docGen.default'), - 'GuardrailsException' + 'GuardrailsException', + codeGenerationRemainingIterationCount ) } case codegenResult.codeGenerationStatusDetail?.includes('EmptyPatch'): { @@ -186,7 +187,8 @@ abstract class CodeGenBase { case codegenResult.codeGenerationStatusDetail?.includes('Throttling'): { throw new DocServiceError( i18n('AWS.amazonq.featureDev.error.throttling'), - 'ThrottlingException' + 'ThrottlingException', + codeGenerationRemainingIterationCount ) } default: {