Skip to content

Commit 712d978

Browse files
tgodara-awslaileni-awschungjac
authored
feat(amazonq): display transformation history and add ability to resume interrupted jobs (#7781)
## Problem Users cannot see previous job details (status, project name, job id, etc) and cannot access the final diff patch. Network issues can cause jobs to fail on client side while they continue on the backend, and users have no way to access those artifacts. ## Solution Repurpose the job status table to show most recent 10 jobs run in the last 30 days, including links to final diff patch and summary files. Allow users to retrieve missing artifacts for jobs and to resume incomplete jobs via refresh button. <img width="1133" height="290" alt="History Table - New Text" src="https://github.com/user-attachments/assets/5aea4f3d-5fd6-4109-997a-af7ed737ea32" /> <img width="313" height="274" alt="View History Button" src="https://github.com/user-attachments/assets/216ee214-df39-4e95-bf03-1f9f448faf1f" /> --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Laxman Reddy <[email protected]> Co-authored-by: chungjac <[email protected]>
1 parent 639d035 commit 712d978

File tree

14 files changed

+1120
-39
lines changed

14 files changed

+1120
-39
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "/transform: Show transformation history in Transformation Hub and allow users to resume jobs"
4+
}

packages/amazonq/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@
746746
},
747747
{
748748
"command": "aws.amazonq.showHistoryInHub",
749-
"title": "%AWS.command.q.transform.viewJobStatus%"
749+
"title": "%AWS.command.q.transform.viewJobHistory%"
750750
},
751751
{
752752
"command": "aws.amazonq.selectCustomization",

packages/core/package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@
281281
"AWS.command.q.transform.rejectChanges": "Reject",
282282
"AWS.command.q.transform.stopJobInHub": "Stop job",
283283
"AWS.command.q.transform.viewJobProgress": "View job progress",
284-
"AWS.command.q.transform.viewJobStatus": "View job status",
284+
"AWS.command.q.transform.viewJobHistory": "View job history",
285285
"AWS.command.q.transform.showTransformationPlan": "View plan",
286286
"AWS.command.q.transform.showChangeSummary": "View summary",
287287
"AWS.command.threatComposer.createNew": "Create New Threat Composer File",

packages/core/src/amazonqGumby/activation.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { setContext } from '../shared/vscode/setContext'
2121
export async function activate(context: ExtContext) {
2222
void setContext('gumby.wasQCodeTransformationUsed', false)
2323

24-
const transformationHubViewProvider = new TransformationHubViewProvider()
24+
const transformationHubViewProvider = TransformationHubViewProvider.instance
2525
new ProposedTransformationExplorer(context.extensionContext)
2626
// Register an activation event listener to determine when the IDE opens, closes or users
2727
// select to open a new workspace
@@ -72,6 +72,13 @@ export async function activate(context: ExtContext) {
7272
)
7373
}),
7474

75+
Commands.register(
76+
'aws.amazonq.transformationHub.updateContent',
77+
async (button, startTime, historyFileUpdated) => {
78+
await transformationHubViewProvider.updateContent(button, startTime, historyFileUpdated)
79+
}
80+
),
81+
7582
workspaceChangeEvent
7683
)
7784
}

packages/core/src/amazonqGumby/chat/controller/controller.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ import {
5757
} from '../../../codewhisperer/service/transformByQ/transformFileHandler'
5858
import { getAuthType } from '../../../auth/utils'
5959
import fs from '../../../shared/fs/fs'
60+
import { setContext } from '../../../shared/vscode/setContext'
61+
import { readHistoryFile } from '../../../codewhisperer/service/transformByQ/transformationHubViewProvider'
6062

6163
// These events can be interactions within the chat,
6264
// or elsewhere in the IDE
@@ -188,6 +190,15 @@ export class GumbyController {
188190
}
189191

190192
private async transformInitiated(message: any) {
193+
// check if any jobs potentially still in progress on backend
194+
const history = await readHistoryFile()
195+
const numInProgress = history.filter((job) => job.status === 'FAILED').length
196+
this.messenger.sendViewHistoryMessage(message.tabID, numInProgress)
197+
if (transformByQState.isRefreshInProgress()) {
198+
this.messenger.sendMessage(CodeWhispererConstants.refreshInProgressChatMessage, message.tabID, 'ai-prompt')
199+
return
200+
}
201+
191202
// silently check for projects eligible for SQL conversion
192203
let embeddedSQLProjects: TransformationCandidateProject[] = []
193204
try {
@@ -383,6 +394,11 @@ export class GumbyController {
383394
case ButtonActions.VIEW_TRANSFORMATION_HUB:
384395
await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB, CancelActionPositions.Chat)
385396
break
397+
case ButtonActions.VIEW_JOB_HISTORY:
398+
await setContext('gumby.wasQCodeTransformationUsed', true)
399+
await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB)
400+
await vscode.commands.executeCommand(GumbyCommands.FOCUS_JOB_HISTORY, CancelActionPositions.Chat)
401+
break
386402
case ButtonActions.VIEW_SUMMARY:
387403
await vscode.commands.executeCommand('aws.amazonq.transformationHub.summary.reveal')
388404
break
@@ -452,6 +468,10 @@ export class GumbyController {
452468
}
453469

454470
private async handleUserLanguageUpgradeProjectChoice(message: any) {
471+
if (transformByQState.isRefreshInProgress()) {
472+
this.messenger.sendMessage(CodeWhispererConstants.refreshInProgressChatMessage, message.tabID, 'ai-prompt')
473+
return
474+
}
455475
await telemetry.codeTransform_submitSelection.run(async () => {
456476
const pathToProject: string = message.formSelectedValues['GumbyTransformLanguageUpgradeProjectForm']
457477
const toJDKVersion: JDKVersion = message.formSelectedValues['GumbyTransformJdkToForm']
@@ -484,6 +504,10 @@ export class GumbyController {
484504
}
485505

486506
private async handleUserSQLConversionProjectSelection(message: any) {
507+
if (transformByQState.isRefreshInProgress()) {
508+
this.messenger.sendMessage(CodeWhispererConstants.refreshInProgressChatMessage, message.tabID, 'ai-prompt')
509+
return
510+
}
487511
await telemetry.codeTransform_submitSelection.run(async () => {
488512
const pathToProject: string = message.formSelectedValues['GumbyTransformSQLConversionProjectForm']
489513
const schema: string = message.formSelectedValues['GumbyTransformSQLSchemaForm']

packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,38 @@ export class Messenger {
377377
this.dispatcher.sendChatMessage(jobSubmittedMessage)
378378
}
379379

380+
public sendViewHistoryMessage(tabID: string, numInProgress: number) {
381+
const buttons: ChatItemButton[] = []
382+
383+
buttons.push({
384+
keepCardAfterClick: true,
385+
text: CodeWhispererConstants.jobHistoryButtonText,
386+
id: ButtonActions.VIEW_JOB_HISTORY,
387+
disabled: false,
388+
})
389+
390+
const messageText = CodeWhispererConstants.viewHistoryMessage(numInProgress)
391+
392+
const message = new ChatMessage(
393+
{
394+
message: messageText,
395+
messageType: 'ai-prompt',
396+
buttons,
397+
},
398+
tabID
399+
)
400+
this.dispatcher.sendChatMessage(message)
401+
}
402+
403+
public sendJobRefreshInProgressMessage(tabID: string, jobId: string) {
404+
this.dispatcher.sendAsyncEventProgress(
405+
new AsyncEventProgressMessage(tabID, {
406+
inProgress: true,
407+
message: CodeWhispererConstants.refreshingJobChatMessage(jobId),
408+
})
409+
)
410+
}
411+
380412
public sendMessage(prompt: string, tabID: string, type: 'prompt' | 'ai-prompt') {
381413
this.dispatcher.sendChatMessage(
382414
new ChatMessage(

packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import DependencyVersions from '../../../models/dependencies'
1313
export enum ButtonActions {
1414
STOP_TRANSFORMATION_JOB = 'gumbyStopTransformationJob',
1515
VIEW_TRANSFORMATION_HUB = 'gumbyViewTransformationHub',
16+
VIEW_JOB_HISTORY = 'gumbyViewJobHistory',
1617
VIEW_SUMMARY = 'gumbyViewSummary',
1718
CONFIRM_LANGUAGE_UPGRADE_TRANSFORMATION_FORM = 'gumbyLanguageUpgradeTransformFormConfirm',
1819
CONFIRM_SQL_CONVERSION_TRANSFORMATION_FORM = 'gumbySQLConversionTransformFormConfirm',
@@ -33,6 +34,7 @@ export enum GumbyCommands {
3334
CLEAR_CHAT = 'aws.awsq.clearchat',
3435
START_TRANSFORMATION_FLOW = 'aws.awsq.transform',
3536
FOCUS_TRANSFORMATION_HUB = 'aws.amazonq.showTransformationHub',
37+
FOCUS_JOB_HISTORY = 'aws.amazonq.showHistoryInHub',
3638
}
3739

3840
export default class MessengerUtils {

packages/core/src/codewhisperer/commands/startTransformByQ.ts

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
TransformationType,
2121
TransformationCandidateProject,
2222
RegionProfile,
23+
sessionJobHistory,
2324
} from '../models/model'
2425
import {
2526
createZipManifest,
@@ -474,6 +475,30 @@ export async function startTransformationJob(
474475
codeTransformRunTimeLatency: calculateTotalLatency(transformStartTime),
475476
})
476477
})
478+
479+
// create local history folder(s) and store metadata
480+
const jobHistoryPath = path.join(os.homedir(), '.aws', 'transform', transformByQState.getProjectName(), jobId)
481+
if (!fs.existsSync(jobHistoryPath)) {
482+
fs.mkdirSync(jobHistoryPath, { recursive: true })
483+
}
484+
transformByQState.setJobHistoryPath(jobHistoryPath)
485+
// save a copy of the upload zip
486+
fs.copyFileSync(transformByQState.getPayloadFilePath(), path.join(jobHistoryPath, 'zipped-code.zip'))
487+
488+
const fields = [
489+
jobId,
490+
transformByQState.getTransformationType(),
491+
transformByQState.getSourceJDKVersion(),
492+
transformByQState.getTargetJDKVersion(),
493+
transformByQState.getCustomDependencyVersionFilePath(),
494+
transformByQState.getCustomBuildCommand(),
495+
transformByQState.getTargetJavaHome(),
496+
transformByQState.getProjectPath(),
497+
transformByQState.getStartTime(),
498+
]
499+
500+
const jobDetails = fields.join('\t')
501+
fs.writeFileSync(path.join(jobHistoryPath, 'metadata.txt'), jobDetails)
477502
} catch (error) {
478503
getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToStartJobNotification}`, error)
479504
const errorMessage = (error as Error).message.toLowerCase()
@@ -724,9 +749,18 @@ export async function postTransformationJob() {
724749
})
725750
}
726751

727-
if (transformByQState.getPayloadFilePath()) {
728-
// delete original upload ZIP at very end of transformation
729-
fs.rmSync(transformByQState.getPayloadFilePath(), { force: true })
752+
// delete original upload ZIP at very end of transformation
753+
fs.rmSync(transformByQState.getPayloadFilePath(), { force: true })
754+
755+
if (
756+
transformByQState.isSucceeded() ||
757+
transformByQState.isPartiallySucceeded() ||
758+
transformByQState.isCancelled()
759+
) {
760+
// delete the copy of the upload ZIP
761+
fs.rmSync(path.join(transformByQState.getJobHistoryPath(), 'zipped-code.zip'), { force: true })
762+
// delete transformation job metadata file (no longer needed)
763+
fs.rmSync(path.join(transformByQState.getJobHistoryPath(), 'metadata.txt'), { force: true })
730764
}
731765
// delete temporary build logs file
732766
const logFilePath = path.join(os.tmpdir(), 'build-logs.txt')
@@ -739,31 +773,52 @@ export async function postTransformationJob() {
739773
if (transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded()) {
740774
await vscode.commands.executeCommand('aws.amazonq.transformationHub.reviewChanges.startReview')
741775
}
776+
777+
// store job details and diff path locally (history)
778+
// TODO: ideally when job is cancelled, should be stored as CANCELLED instead of FAILED (remove this if statement after bug is fixed)
779+
if (!transformByQState.isCancelled()) {
780+
const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv')
781+
// create transform folder if necessary
782+
if (!fs.existsSync(historyLogFilePath)) {
783+
fs.mkdirSync(path.dirname(historyLogFilePath), { recursive: true })
784+
// create headers of new transformation history file
785+
fs.writeFileSync(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n')
786+
}
787+
const latest = sessionJobHistory[transformByQState.getJobId()]
788+
const fields = [
789+
latest.startTime,
790+
latest.projectName,
791+
latest.status,
792+
latest.duration,
793+
transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded()
794+
? path.join(transformByQState.getJobHistoryPath(), 'diff.patch')
795+
: '',
796+
transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded()
797+
? path.join(transformByQState.getJobHistoryPath(), 'summary', 'summary.md')
798+
: '',
799+
transformByQState.getJobId(),
800+
]
801+
802+
const jobDetails = fields.join('\t') + '\n'
803+
fs.writeFileSync(historyLogFilePath, jobDetails, { flag: 'a' }) // 'a' flag used to append to file
804+
await vscode.commands.executeCommand(
805+
'aws.amazonq.transformationHub.updateContent',
806+
'job history',
807+
undefined,
808+
true
809+
)
810+
}
742811
}
743812

744813
export async function transformationJobErrorHandler(error: any) {
745814
if (!transformByQState.isCancelled()) {
746815
// means some other error occurred; cancellation already handled by now with stopTransformByQ
747-
await stopJob(transformByQState.getJobId())
748816
transformByQState.setToFailed()
749817
transformByQState.setPolledJobStatus('FAILED')
750818
// jobFailureErrorNotification should always be defined here
751-
const displayedErrorMessage =
752-
transformByQState.getJobFailureErrorNotification() ?? CodeWhispererConstants.failedToCompleteJobNotification
753819
transformByQState.setJobFailureErrorChatMessage(
754820
transformByQState.getJobFailureErrorChatMessage() ?? CodeWhispererConstants.failedToCompleteJobChatMessage
755821
)
756-
void vscode.window
757-
.showErrorMessage(displayedErrorMessage, CodeWhispererConstants.amazonQFeedbackText)
758-
.then((choice) => {
759-
if (choice === CodeWhispererConstants.amazonQFeedbackText) {
760-
void submitFeedback(
761-
placeholder,
762-
CodeWhispererConstants.amazonQFeedbackKey,
763-
getFeedbackCommentData()
764-
)
765-
}
766-
})
767822
} else {
768823
transformByQState.setToCancelled()
769824
transformByQState.setPolledJobStatus('CANCELLED')

packages/core/src/codewhisperer/models/constants.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,34 @@ export const formattedStringMap = new Map([
799799
['numChangedFiles', 'Files to be changed'],
800800
])
801801

802+
export const refreshInProgressChatMessage = 'A job refresh is currently in progress. Please wait for it to complete.'
803+
804+
export const refreshingJobChatMessage = (jobId: string) =>
805+
`I am now resuming your job (id: ${jobId}). This can take 10 to 30 minutes to complete.`
806+
807+
export const jobHistoryButtonText = 'Open job history'
808+
809+
export const viewHistoryMessage = (numInProgress: number) =>
810+
numInProgress > 0
811+
? `You have ${numInProgress} job${numInProgress > 1 ? 's' : ''} in progress. You can resume ${numInProgress > 1 ? 'them' : 'it'} in the transformation history table.`
812+
: 'View previous transformations run from the IDE'
813+
814+
export const transformationHistoryTableDescription =
815+
'This table lists the most recent jobs that you have run in the past 30 days. To open the diff patch and summary files, click the provided links. To get an updated job status, click the refresh icon. The diff patch and summary will appear once they are available.<br><br>' +
816+
'Jobs with a status of FAILED may still be in progress. Resume these jobs within 12 hours of starting the job to get an updated job status and artifacts.'
817+
818+
export const refreshErrorChatMessage =
819+
"Sorry, I couldn't refresh the job. Please try again or start a new transformation."
820+
821+
export const refreshErrorNotification = (jobId: string) => `There was an error refreshing this job. Job Id: ${jobId}`
822+
823+
export const refreshCompletedChatMessage =
824+
'Job refresh completed. Please see the transformation history table for the updated status and artifacts.'
825+
826+
export const refreshCompletedNotification = (jobId: string) => `Job refresh completed. (Job Id: ${jobId})`
827+
828+
export const refreshNoUpdatesNotification = (jobId: string) => `No updates. (Job Id: ${jobId})`
829+
802830
// end of QCT Strings
803831

804832
export enum UserGroup {
@@ -912,3 +940,14 @@ export const codeReviewFindingsSuffix = '_codeReviewFindings'
912940
export const displayFindingsSuffix = '_displayFindings'
913941

914942
export const displayFindingsDetectorName = 'DisplayFindings'
943+
export const findingsSuffix = '_codeReviewFindings'
944+
945+
export interface HistoryObject {
946+
startTime: string
947+
projectName: string
948+
status: string
949+
duration: string
950+
diffPath: string
951+
summaryPath: string
952+
jobId: string
953+
}

0 commit comments

Comments
 (0)