diff --git a/packages/amazonq/.changes/next-release/Bug Fix-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json b/packages/amazonq/.changes/next-release/Bug Fix-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json new file mode 100644 index 00000000000..3883fe075a2 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Bug Fix-dd58d9cb-4fb4-4cb3-a9a2-dc5c5462e529.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q (/dev): view diffs of previous /dev iterations" +} diff --git a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts index 2af763ecf11..f2a08348d23 100644 --- a/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts +++ b/packages/amazonq/test/unit/amazonqFeatureDev/session/session.test.ts @@ -96,7 +96,8 @@ describe('session', () => { [], [], tabID, - 0 + 0, + {} ) const session = await createSession({ messenger, sessionState: codeGenState, conversationID }) encodedContent = new TextEncoder().encode(notRejectedFileContent) diff --git a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts index 37657c803cf..4a31fe337c4 100644 --- a/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/featureDevChatConnector.ts @@ -94,12 +94,13 @@ export class Connector { }) } - onOpenDiff = (tabID: string, filePath: string, deleted: boolean): void => { + onOpenDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { this.sendMessageToExtension({ command: 'open-diff', tabID, filePath, deleted, + messageId, tabType: 'featuredev', }) } @@ -167,7 +168,11 @@ export class Connector { canBeVoted: true, codeReference: messageData.references, // TODO get the backend to store a message id in addition to conversationID - messageId: messageData.messageID ?? messageData.triggerID ?? messageData.conversationID, + messageId: + messageData.codeGenerationId ?? + messageData.messageID ?? + messageData.triggerID ?? + messageData.conversationID, fileList: { rootFolderTitle: 'Changes', filePaths: messageData.filePaths.map((f: DiffTreeFileInfo) => f.zipFilePath), diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 63504c67ceb..8062672442e 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -24,6 +24,16 @@ export interface CodeReference { } } +export interface UploadHistory { + [key: string]: { + uploadId: string + timestamp: number + tabId: string + filePaths: DiffTreeFileInfo[] + deletedFiles: DiffTreeFileInfo[] + } +} + export interface ChatPayload { chatMessage: string chatCommand?: string @@ -355,10 +365,10 @@ export class Connector { } } - onOpenDiff = (tabID: string, filePath: string, deleted: boolean): void => { + onOpenDiff = (tabID: string, filePath: string, deleted: boolean, messageId?: string): void => { switch (this.tabsStorage.getTab(tabID)?.type) { case 'featuredev': - this.featureDevChatConnector.onOpenDiff(tabID, filePath, deleted) + this.featureDevChatConnector.onOpenDiff(tabID, filePath, deleted, messageId) break } } diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index 292bc2cc848..53a2b56e773 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -67,6 +67,7 @@ type OpenDiffMessage = { // currently the zip file path filePath: string deleted: boolean + codeGenerationId: string } type fileClickedMessage = { @@ -400,7 +401,8 @@ export class FeatureDevController { deletedFiles, session.state.references ?? [], tabID, - session.uploadId + session.uploadId, + session.state.codeGenerationId ?? '' ) const remainingIterations = session.state.codeGenerationRemainingIterationCount @@ -686,6 +688,7 @@ export class FeatureDevController { private async openDiff(message: OpenDiffMessage) { const tabId: string = message.tabID + const codeGenerationId: string = message.messageId const zipFilePath: string = message.filePath const session = await this.sessionStorage.getSession(tabId) telemetry.amazonq_isReviewedChanges.emit({ @@ -702,7 +705,11 @@ export class FeatureDevController { const name = path.basename(pathInfos.relativePath) await openDeletedDiff(pathInfos.absolutePath, name, tabId) } else { - const rightPath = path.join(session.uploadId, zipFilePath) + let uploadId = session.uploadId + if (session?.state?.uploadHistory && session.state.uploadHistory[codeGenerationId]) { + uploadId = session?.state?.uploadHistory[codeGenerationId].uploadId + } + const rightPath = path.join(uploadId, zipFilePath) await openDiff(pathInfos.absolutePath, rightPath, tabId) } } diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts index a1902bd8472..78d436a7e34 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts @@ -115,9 +115,12 @@ export class Messenger { deletedFiles: DeletedFileInfo[], references: CodeReference[], tabID: string, - uploadId: string + uploadId: string, + codeGenerationId: string ) { - this.dispatcher.sendCodeResult(new CodeResultMessage(filePaths, deletedFiles, references, tabID, uploadId)) + this.dispatcher.sendCodeResult( + new CodeResultMessage(filePaths, deletedFiles, references, tabID, uploadId, codeGenerationId) + ) } public sendAsyncEventProgress(tabID: string, inProgress: boolean, message: string | undefined) { diff --git a/packages/core/src/amazonqFeatureDev/session/session.ts b/packages/core/src/amazonqFeatureDev/session/session.ts index 01e6c9a2e45..27f26b18d15 100644 --- a/packages/core/src/amazonqFeatureDev/session/session.ts +++ b/packages/core/src/amazonqFeatureDev/session/session.ts @@ -130,6 +130,7 @@ export class Session { fs: this.config.fs, messenger: this.messenger, telemetry: this.telemetry, + uploadHistory: this.state.uploadHistory, }) if (resp.nextState) { diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts index c26f1342e0c..a2204bb9901 100644 --- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts +++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts @@ -36,7 +36,7 @@ import { import { prepareRepoData } from '../util/files' import { TelemetryHelper } from '../util/telemetryHelper' import { uploadCode } from '../util/upload' -import { CodeReference } from '../../amazonq/webview/ui/connector' +import { CodeReference, UploadHistory } from '../../amazonq/webview/ui/connector' import { isPresent } from '../../shared/utilities/collectionUtils' import { AuthUtil } from '../../codewhisperer/util/authUtil' import { randomUUID } from '../../shared/crypto' @@ -261,6 +261,7 @@ export class CodeGenState extends CodeGenBase implements SessionState { public references: CodeReference[], tabID: string, private currentIteration: number, + public uploadHistory: UploadHistory, public codeGenerationRemainingIterationCount?: number, public codeGenerationTotalIterationCount?: number ) { @@ -308,6 +309,16 @@ export class CodeGenState extends CodeGenBase implements SessionState { this.codeGenerationRemainingIterationCount = codeGeneration.codeGenerationRemainingIterationCount this.codeGenerationTotalIterationCount = codeGeneration.codeGenerationTotalIterationCount + if (action.uploadHistory && !action.uploadHistory[codeGenerationId] && codeGenerationId) { + action.uploadHistory[codeGenerationId] = { + timestamp: Date.now(), + uploadId: this.config.uploadId, + filePaths: codeGeneration.newFiles, + deletedFiles: codeGeneration.deletedFiles, + tabId: this.tabID, + } + } + action.telemetry.setAmazonqNumberOfReferences(this.references.length) action.telemetry.recordUserCodeGenerationTelemetry(span, this.conversationId) const nextState = new PrepareCodeGenState( @@ -318,7 +329,9 @@ export class CodeGenState extends CodeGenBase implements SessionState { this.tabID, this.currentIteration + 1, this.codeGenerationRemainingIterationCount, - this.codeGenerationTotalIterationCount + this.codeGenerationTotalIterationCount, + action.uploadHistory, + codeGenerationId ) return { nextState, @@ -338,6 +351,7 @@ export class MockCodeGenState implements SessionState { public filePaths: NewFileInfo[] public deletedFiles: DeletedFileInfo[] public readonly conversationId: string + public readonly codeGenerationId?: string public readonly uploadId: string constructor( @@ -384,7 +398,8 @@ export class MockCodeGenState implements SessionState { }, ], this.tabID, - this.uploadId + this.uploadId, + this.codeGenerationId ?? '' ) action.messenger.sendAnswer({ message: undefined, @@ -431,11 +446,15 @@ export class PrepareCodeGenState implements SessionState { public tabID: string, private currentIteration: number, public codeGenerationRemainingIterationCount?: number, - public codeGenerationTotalIterationCount?: number + public codeGenerationTotalIterationCount?: number, + public uploadHistory: UploadHistory = {}, + public codeGenerationId?: string ) { this.tokenSource = new vscode.CancellationTokenSource() this.uploadId = config.uploadId this.conversationId = config.conversationId + this.uploadHistory = uploadHistory + this.codeGenerationId = codeGenerationId } updateWorkspaceRoot(workspaceRoot: string) { @@ -490,7 +509,8 @@ export class PrepareCodeGenState implements SessionState { this.deletedFiles, this.references, this.tabID, - this.currentIteration + this.currentIteration, + this.uploadHistory ) return nextState.interact(action) } diff --git a/packages/core/src/amazonqFeatureDev/types.ts b/packages/core/src/amazonqFeatureDev/types.ts index fafe26a9e24..75564350882 100644 --- a/packages/core/src/amazonqFeatureDev/types.ts +++ b/packages/core/src/amazonqFeatureDev/types.ts @@ -9,7 +9,7 @@ import type { CancellationTokenSource } from 'vscode' import { Messenger } from './controllers/chat/messenger/messenger' import { FeatureDevClient } from './client/featureDev' import { TelemetryHelper } from './util/telemetryHelper' -import { CodeReference } from '../amazonq/webview/ui/connector' +import { CodeReference, UploadHistory } from '../amazonq/webview/ui/connector' import { DiffTreeFileInfo } from '../amazonq/webview/ui/diffTree/types' export type Interaction = { @@ -61,11 +61,13 @@ export interface SessionState { readonly phase?: SessionStatePhase readonly uploadId: string readonly tokenSource: CancellationTokenSource + readonly codeGenerationId?: string readonly tabID: string interact(action: SessionStateAction): Promise updateWorkspaceRoot?: (workspaceRoot: string) => void codeGenerationRemainingIterationCount?: number codeGenerationTotalIterationCount?: number + uploadHistory?: UploadHistory } export interface SessionStateConfig { @@ -82,6 +84,7 @@ export interface SessionStateAction { messenger: Messenger fs: VirtualFileSystem telemetry: TelemetryHelper + uploadHistory?: UploadHistory } export type NewFileZipContents = { zipFilePath: string; fileContent: string } diff --git a/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts b/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts index 790dd1a6e05..33ed5205c91 100644 --- a/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts +++ b/packages/core/src/amazonqFeatureDev/views/actions/uiMessageListener.ts @@ -108,6 +108,7 @@ export class UIMessageListener { tabID: msg.tabID, filePath: msg.filePath, deleted: msg.deleted, + messageId: msg.messageId, }) } diff --git a/packages/core/src/amazonqFeatureDev/views/connector/connector.ts b/packages/core/src/amazonqFeatureDev/views/connector/connector.ts index 06cba2b6256..9d1e681bf9c 100644 --- a/packages/core/src/amazonqFeatureDev/views/connector/connector.ts +++ b/packages/core/src/amazonqFeatureDev/views/connector/connector.ts @@ -33,6 +33,7 @@ export class ErrorMessage extends UiMessage { export class CodeResultMessage extends UiMessage { readonly message!: string + readonly codeGenerationId!: string readonly references!: { information: string recommendationContentSpan: { @@ -48,7 +49,8 @@ export class CodeResultMessage extends UiMessage { readonly deletedFiles: DeletedFileInfo[], references: CodeReference[], tabID: string, - conversationID: string + conversationID: string, + codeGenerationId: string ) { super(tabID) this.references = references @@ -64,6 +66,7 @@ export class CodeResultMessage extends UiMessage { }, } }) + this.codeGenerationId = codeGenerationId this.conversationID = conversationID } } diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts index 67ffb2029e2..1f72aa6f270 100644 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts @@ -263,7 +263,7 @@ describe('Controller', () => { workspaceFolders, } - const codeGenState = new CodeGenState(testConfig, getFilePaths(controllerSetup), [], [], tabID, 0) + const codeGenState = new CodeGenState(testConfig, getFilePaths(controllerSetup), [], [], tabID, 0, {}) const newSession = await createSession({ messenger: controllerSetup.messenger, sessionState: codeGenState, diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts index 91833383073..c62266364f8 100644 --- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts @@ -27,10 +27,10 @@ const mockSessionStateAction = (msg?: string): SessionStateAction => { new AppToWebViewMessageDispatcher(new MessagePublisher(new vscode.EventEmitter())) ), telemetry: new TelemetryHelper(), + uploadHistory: {}, } } -let mockGeneratePlan: sinon.SinonStub let mockGetCodeGeneration: sinon.SinonStub let mockExportResultArchive: sinon.SinonStub let mockCreateUploadUrl: sinon.SinonStub @@ -49,7 +49,6 @@ const mockSessionStateConfig = ({ proxyClient: { createConversation: () => sinon.stub(), createUploadUrl: () => mockCreateUploadUrl(), - generatePlan: () => mockGeneratePlan(), startCodeGeneration: () => sinon.stub(), getCodeGeneration: () => mockGetCodeGeneration(), exportResultArchive: () => mockExportResultArchive(), @@ -111,7 +110,7 @@ describe('sessionState', () => { mockExportResultArchive = sinon.stub().resolves({ newFileContents: [], deletedFiles: [], references: [] }) const testAction = mockSessionStateAction() - const state = new CodeGenState(testConfig, [], [], [], tabId, 0, 2, 3) + const state = new CodeGenState(testConfig, [], [], [], tabId, 0, {}, 2, 3) const result = await state.interact(testAction) const nextState = new PrepareCodeGenState(testConfig, [], [], [], tabId, 1, 2, 3) @@ -125,7 +124,7 @@ describe('sessionState', () => { it('fails when codeGenerationStatus failed ', async () => { mockGetCodeGeneration = sinon.stub().rejects(new ToolkitError('Code generation failed')) const testAction = mockSessionStateAction() - const state = new CodeGenState(testConfig, [], [], [], tabId, 0) + const state = new CodeGenState(testConfig, [], [], [], tabId, 0, {}) try { await state.interact(testAction) assert.fail('failed code generations should throw an error')