Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
aa7f73b
feat(backend): implement backward-compatible generator progress notif…
korotkovao Sep 11, 2026
14655b8
fix(backend): update test mocks to implement new YouiEvents interface
korotkovao Sep 11, 2026
4bfa059
fix(test): update doGeneratorInstall test mock to include cancellable…
korotkovao Sep 11, 2026
8275a15
feat(backend): implement backward-compatible generator progress notif…
korotkovao Sep 11, 2026
8f99fed
test(backend): add backward compatibility tests for doGeneratorProgress
korotkovao Sep 11, 2026
4392cd9
fix(backend): address code review issues - race conditions and messag…
korotkovao Sep 14, 2026
2a29abe
fix(backend): add explicit return type annotation to arrow function
korotkovao Sep 14, 2026
3d7324f
fix(backend): address code review - race conditions and state management
korotkovao Sep 14, 2026
cf06f7c
test(yeoman-ui): increase test coverage to meet 92% threshold
korotkovao Sep 14, 2026
198ea25
fix(yeoman-ui): address review feedback for backward-compatible progr…
korotkovao Sep 23, 2026
a3aa8b1
test(yeoman-ui): add tests for progress notification review fixes
korotkovao Sep 23, 2026
f11a28d
fix(yeoman-ui): correct TestRpc to extend RpcCommon class
korotkovao Sep 23, 2026
4a0a28f
test(yeoman-ui): add frontend tests for generatorProgress method
korotkovao Sep 23, 2026
4e2324d
fix(yeoman-ui): restore ApplicationWizard.showGeneratorProgress setti…
korotkovao Sep 23, 2026
c673820
fix(yeoman-ui): add ApplicationWizard.showGeneratorProgress setting d…
korotkovao Sep 23, 2026
9fecf82
Revert "fix(yeoman-ui): add ApplicationWizard.showGeneratorProgress s…
korotkovao Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions projects/yeoman-ui/packages/backend/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export default {
`The '${artifactName}' project has been generated.`,
show_progress_button: "Open Output View",
show_progress_message: "Generating...",
progress_preparing: "Preparing...",
progress_writing_files: "Creating project files...",
progress_installing: "Installing dependencies...",
progress_finalising: "Finalising...",
add_to_workspace: "Open the project in a multi-root workspace",
open_in_a_new_workspace: "Open the project in a stand-alone",
create_and_close: "Create the project and close it for future use",
Expand Down
219 changes: 198 additions & 21 deletions projects/yeoman-ui/packages/backend/src/vscode-youi-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@ export class VSCodeYouiEvents implements YouiEvents {
private readonly rpc: IRpc;
private webviewPanel: WebviewPanel;
private readonly messages: any;
private resolveFunc: any;
private resolveFunc: (() => void) | undefined;
private progressReporter: {
report(value: { message?: string; increment?: number }): void;
} | null = null;
private currentProjectName: string | undefined;
private phaseStartTime: number = 0;
private currentPhase: "writing" | "install" | "end" | null = null;
private isEnhancedProgressMode: boolean = false; // Track if enhanced progress is active
private pendingPhaseTimer: NodeJS.Timeout | null = null; // Track pending timer to allow cancellation
public output: GeneratorOutput;
private readonly logger: IChildLogger;
private readonly appWizard: AppWizard;
Expand Down Expand Up @@ -88,27 +96,134 @@ export class VSCodeYouiEvents implements YouiEvents {
void this.rpc.invoke("setBanner", [bannerProps]);
}

public doGeneratorDone(
public async doGeneratorDone(
success: boolean,
message: string,
selectedWorkspace: string,
type: string,
targetFolderPath?: string
): void {
): Promise<any> {
// Cancel any pending phase transition timer
if (this.pendingPhaseTimer) {
clearTimeout(this.pendingPhaseTimer);
this.pendingPhaseTimer = null;
}

// Show "Finalising..." before closing (only if enhanced progress mode is active)
if (this.progressReporter && this.isEnhancedProgressMode) {
this.progressReporter.report({
message: this.messages.progress_finalising,
});
// Add a brief delay so "Finalising..." is visible to users
await new Promise((resolve) => setTimeout(resolve, 1000));
}

this.resolveInstallingProgress();
set(this.webviewPanel, Constants.GENERATOR_COMPLETED, success);
this.doClose();
void this.showDoneMessage(
return this.showDoneMessage(
success,
message,
selectedWorkspace,
type,
targetFolderPath
targetFolderPath,
true // Skip resolving progress since we already did it
);
}

public doGeneratorInstall(): void {
this.doClose();
this.showInstallMessage();
this.isEnhancedProgressMode = false; // Classic mode
// Classic mode: pass empty string to avoid duplicate message
this.showInstallMessage(undefined, "");
}

public doGeneratorProgress(
projectName: string | undefined,
phase: "writing" | "install" | "end",
showProgress: boolean = false
): void {
// Check VS Code setting (default: true)
// Note: This setting is not declared in yeoman-ui's package.json - consumers
// (e.g., application-modeler, app-generator) declare it in their package.json
// if they want users to control it. The true default is safe - actual gating
// happens via the per-generator showProgress option passed by consumers.
const config = vscode.workspace.getConfiguration();
const settingEnabled = config.get<boolean>(
"ApplicationWizard.showGeneratorProgress",
true
);

// Backward compatibility: if setting is disabled OR generator doesn't opt in,
// fall back to classic behavior (only show toast on "install" phase)
if (!settingEnabled || !showProgress) {
if (phase === "install") {
// Show classic "Installing dependencies..." toast
this.doGeneratorInstall();
}
return;
}

// Enhanced mode: show multi-phase progress
const phaseMessages = {
writing: this.messages.progress_writing_files,
install: this.messages.progress_installing,
end: this.messages.progress_finalising,
};

const MIN_DURATIONS = {
writing: 2000,
install: 0,
end: 1000,
};

const message = phaseMessages[phase];

// If this is the first phase AND no progress notification exists yet
if (!this.progressReporter) {
Comment thread
ilya-taupeka marked this conversation as resolved.
// Close the webview panel (showing the question form) before showing progress
// Set GENERATOR_COMPLETED first to prevent false "manual close" telemetry
set(this.webviewPanel, Constants.GENERATOR_COMPLETED, true);
this.doClose();
this.isEnhancedProgressMode = true; // Enhanced mode active
this.currentPhase = phase;
this.phaseStartTime = Date.now();
this.showInstallMessage(projectName, message);
} else if (this.progressReporter) {
// Cancel any pending phase transition timer before scheduling a new one
if (this.pendingPhaseTimer) {
clearTimeout(this.pendingPhaseTimer);
this.pendingPhaseTimer = null;
}

// Calculate time elapsed in current phase
const elapsed = Date.now() - this.phaseStartTime;
const minDuration = this.currentPhase
? MIN_DURATIONS[this.currentPhase]
: 0;
const remainingTime = Math.max(0, minDuration - elapsed);

if (remainingTime > 0) {
Comment thread
ilya-taupeka marked this conversation as resolved.
// Wait for minimum duration before showing next phase
// Capture current reporter instance to prevent race conditions
const currentReporter = this.progressReporter;
this.pendingPhaseTimer = setTimeout(() => {
// Clear the timer reference since it has fired
this.pendingPhaseTimer = null;
// Only update if reporter hasn't changed (generator still running)
if (this.progressReporter === currentReporter) {
this.progressReporter.report({ message });
this.currentPhase = phase;
this.phaseStartTime = Date.now();
}
}, remainingTime);
} else {
// Minimum duration already elapsed, update immediately
this.progressReporter.report({ message });
this.currentPhase = phase;
this.phaseStartTime = Date.now();
}
}
}

public getAppWizard(): AppWizard {
Expand Down Expand Up @@ -182,23 +297,66 @@ export class VSCodeYouiEvents implements YouiEvents {
}
}

private showInstallMessage(): void {
private showInstallMessage(
projectName?: string,
initialMessage: string = this.messages.progress_preparing
): void {
// Store project name for later use in success message
this.currentProjectName = projectName;

// Determine if this is enhanced mode (any of the progress messages)
const isEnhancedMode =
initialMessage === this.messages.progress_preparing ||
initialMessage === this.messages.progress_writing_files ||
initialMessage === this.messages.progress_installing ||
initialMessage === this.messages.progress_finalising;

// Determine title:
// - Enhanced mode: "Generating {projectName}" or "Generating..."
// - Classic mode: "Installing dependencies..."
const title = isEnhancedMode
? projectName
? `Generating ${projectName}`
: "Generating..."
: "Installing dependencies...";

void vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Installing dependencies...",
title: title,
cancellable: false,
},
async () => {
await new Promise((resolve) => {
async (progress) => {
// Store the progress reporter so we can update it (for new progress system)
this.progressReporter = progress;
// Classic mode: initialMessage is empty string, keep it empty to show title only
// Enhanced mode: initialMessage has a message, show it in body
progress.report({ message: initialMessage });

// Keep the notification open until generation completes
await new Promise<void>((resolve) => {
this.resolveFunc = resolve;
});

// Clean up the progress reporter and reset state for next run
this.progressReporter = null;
this.isEnhancedProgressMode = false;
this.currentPhase = null;
this.phaseStartTime = 0;
this.currentProjectName = undefined;
// Clear any pending phase timer
if (this.pendingPhaseTimer) {
clearTimeout(this.pendingPhaseTimer);
this.pendingPhaseTimer = null;
}
}
);
}

private resolveInstallingProgress() {
if (this.resolveFunc) {
this.resolveFunc();
this.resolveFunc = undefined; // Clear to prevent reuse
}
}

Expand All @@ -207,9 +365,12 @@ export class VSCodeYouiEvents implements YouiEvents {
errorMmessage: string,
selectedWorkspace: string,
type: string,
targetFolderPath?: string
targetFolderPath?: string,
skipResolve: boolean = false
): Thenable<any> {
this.resolveInstallingProgress();
if (!skipResolve) {
this.resolveInstallingProgress();
}

if (success) {
if (!isNil(targetFolderPath)) {
Expand Down Expand Up @@ -319,17 +480,33 @@ export class VSCodeYouiEvents implements YouiEvents {
selectedWorkspace: string,
type: string
): string {
let successInfoMessage: string = this.messages.artifact_generated_files;
// Default message with project name if available
let successInfoMessage: string = this.currentProjectName
? `Project ${this.currentProjectName} has been generated.`
: this.messages.artifact_generated_files;

if (type === "project") {
if (selectedWorkspace === this.messages.open_in_a_new_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_open_in_a_new_workspace;
} else if (selectedWorkspace === this.messages.add_to_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_add_to_workspace;
// For project type, use project name and add workspace-specific detail
if (this.currentProjectName) {
if (selectedWorkspace === this.messages.open_in_a_new_workspace) {
successInfoMessage = `Project ${this.currentProjectName} has been generated. The project will be opened in a new workspace.`;
} else if (selectedWorkspace === this.messages.add_to_workspace) {
successInfoMessage = `Project ${this.currentProjectName} has been generated. The project has been added to workspace.`;
} else {
successInfoMessage = `Project ${this.currentProjectName} has been generated.`;
}
} else {
successInfoMessage =
this.messages.artifact_generated_project_saved_for_future;
// Fallback to original messages if no project name
if (selectedWorkspace === this.messages.open_in_a_new_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_open_in_a_new_workspace;
} else if (selectedWorkspace === this.messages.add_to_workspace) {
successInfoMessage =
this.messages.artifact_generated_project_add_to_workspace;
} else {
successInfoMessage =
this.messages.artifact_generated_project_saved_for_future;
}
}
} else if (type === "module") {
successInfoMessage = this.messages.artifact_generated_module;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,38 @@ export class ServerYouiEvents implements YouiEvents {
selectedWorkspace: string,
type: string,
targetPath = ""
): void {
void this.rpc.invoke("generatorDone", [
): Promise<void> {
return this.rpc.invoke("generatorDone", [
suceeded,
message,
selectedWorkspace,
type,
targetPath,
]);
]) as Promise<void>;
}

public doGeneratorInstall(): void {
void this.rpc.invoke("generatorInstall");
}

public doGeneratorProgress(
projectName: string | undefined,
phase: "writing" | "install" | "end",
showProgress: boolean = false
): void {
// Backward compatibility: if generator doesn't opt in, fall back to classic behavior
// (only show toast on "install" phase)
if (!showProgress) {
if (phase === "install") {
// Show classic "Installing dependencies..." event
this.doGeneratorInstall();
}
return;
}
// WebSocket implementation - invoke RPC method with progress info
void this.rpc.invoke("generatorProgress", [projectName, phase]);
Comment thread
ilya-taupeka marked this conversation as resolved.
}

public showProgress(): void {
void this.rpc.invoke("showProgress");
}
Expand Down
Loading
Loading