From a8e796fa9bf5c93db54c8212a27e24bbf8d50446 Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Thu, 21 May 2026 16:49:44 +0800 Subject: [PATCH 1/9] Fixed issue 322: app modernization -> GitHub Copilot modernization. --- .../toolkit/intellij/appmod/common/AppModPluginInstaller.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 3640832996..1fe69136f1 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -107,8 +107,8 @@ public static void showInstallConfirmation(@Nonnull Project project, boolean for : "Install this plugin to automate migrating your apps to Azure with Copilot."; } else { message = forUpgrade - ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization." - : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and GitHub Copilot modernization." + : "To migrate to Azure, you'll need two plugins: GitHub Copilot and GitHub Copilot modernization."; } AppModUtils.logTelemetryEvent("plugin." + action + ".install-prompt-shown", Map.of("copilotInstalled", String.valueOf(copilotInstalled))); if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { From 89b4242e7880cb5ff540bf18cd1150ce81e981d2 Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Thu, 21 May 2026 17:11:27 +0800 Subject: [PATCH 2/9] Update for code review. --- .../toolkit/intellij/appmod/common/AppModPluginInstaller.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 1fe69136f1..75972cddeb 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -107,8 +107,8 @@ public static void showInstallConfirmation(@Nonnull Project project, boolean for : "Install this plugin to automate migrating your apps to Azure with Copilot."; } else { message = forUpgrade - ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and GitHub Copilot modernization." - : "To migrate to Azure, you'll need two plugins: GitHub Copilot and GitHub Copilot modernization."; + ? "To upgrade your apps, you'll need to install GitHub Copilot modernization." + : "To migrate to Azure, you'll need to install GitHub Copilot modernization."; } AppModUtils.logTelemetryEvent("plugin." + action + ".install-prompt-shown", Map.of("copilotInstalled", String.valueOf(copilotInstalled))); if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { From e66cce36f7deb8deaab6674880c036831a55e931 Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Tue, 26 May 2026 15:26:28 +0800 Subject: [PATCH 3/9] Fixed https://github.com/devdiv-microsoft/appmod-intellij/issues/324. --- .../javaupgrade/service/JavaVersionNotificationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index 4ea59332a0..ffc8ee3dab 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -48,7 +48,7 @@ public class JavaVersionNotificationService { private static final String NOTIFICATIONS_ENABLED_KEY = "azure.toolkit.java.version.notifications.enabled"; private static final String DEFERRED_UNTIL_KEY = "azure.toolkit.java.version.deferred_until"; private static final long DEFER_INTERVAL_MS = 10 * 24 * 60 * 60 * 1000L; // 10 days in milliseconds - private static final String DEFAULT_MODEL_NAME = "Claude Sonnet 4.5"; + private static final String DEFAULT_MODEL_NAME = "Claude Sonnet 4.6"; // GitHub Copilot plugin ID private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; From ca957427126a07899edf3c45c06a2a9ffb484ead Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Fri, 29 May 2026 17:12:11 +0800 Subject: [PATCH 4/9] Fixed https://github.com/devdiv-microsoft/appmod-intellij/issues/325. --- .../action/UpgradeActionRegistrar.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java index d0d58a23ce..a41c567e7e 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -6,10 +6,15 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionPlaces; +import com.intellij.openapi.actionSystem.ActionPopupMenu; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.actionSystem.Separator; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.actionSystem.ex.ActionPopupMenuListener; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.ProjectActivity; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; @@ -21,6 +26,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.concurrent.atomic.AtomicBoolean; + /** * Registers the Upgrade action into the GitHub Copilot context menu at runtime. * This is needed because the Copilot plugin creates its context menu groups dynamically. @@ -31,17 +38,67 @@ public class UpgradeActionRegistrar implements ProjectActivity { private static final String UPGRADE_ACTION_ID = "AzureToolkit.JavaUpgradeContextMenu"; private static final String PROJECT_VIEW_POPUP_MENU = "ProjectViewPopupMenu"; + // Application-level guard so we install the popup listener only once per IDE process, + // even if multiple projects are opened (this ProjectActivity runs per-project). + private static final AtomicBoolean POPUP_LISTENER_INSTALLED = new AtomicBoolean(false); + @Nullable @Override public Object execute(@NotNull Project project, @NotNull Continuation continuation) { try{ + // Eager attempt: works on 2nd+ project open within the same IDE process, + // after the GitHub Copilot plugin has populated its dynamic submenu. discoverAndRegisterAction(); + // Lazy fallback (fixes the first-open race): re-attempt the registration + // every time the Project View popup is created. The Copilot submenu is + // guaranteed to exist by the time the user right-clicks, and the call + // is cheap + idempotent thanks to the containsAction guard. + installLazyRegistrationListener(); } catch (Throwable e) { log.error("Failed to register Upgrade action in Copilot context menu.", e); } return Unit.INSTANCE; } + /** + * Installs an application-scoped {@link ActionPopupMenuListener} (only once per IDE + * process) that re-runs {@link #discoverAndRegisterAction()} whenever the Project + * View popup menu is opened. This is the lazy fallback for the first project open + * after IDE launch, where {@link ProjectActivity}s from us and from the GitHub Copilot + * plugin race and our discovery can miss Copilot's not-yet-created submenu. + */ + private void installLazyRegistrationListener() { + if (!POPUP_LISTENER_INSTALLED.compareAndSet(false, true)) { + return; + } + try { + ActionManagerEx.getInstanceEx().addActionPopupMenuListener(new ActionPopupMenuListener() { + @Override + public void actionPopupMenuCreated(@NotNull ActionPopupMenu menu) { + // Only react to the Project View right-click popup; ignore all + // other popups (editor, tool windows, etc.) to keep this cheap. + if (!ActionPlaces.PROJECT_VIEW_POPUP.equals(menu.getPlace())) { + return; + } + try { + discoverAndRegisterAction(); + } catch (Throwable ex) { + log.warn("Lazy registration of Upgrade action into Copilot submenu failed.", ex); + } + } + + @Override + public void actionPopupMenuReleased(@NotNull ActionPopupMenu menu) { + // no-op + } + }, ApplicationManager.getApplication()); + } catch (Throwable e) { + // Roll back the flag so a later project open can try installing again. + POPUP_LISTENER_INSTALLED.set(false); + log.warn("Failed to install lazy registration listener for Upgrade action.", e); + } + } + private void discoverAndRegisterAction() { // Only proceed if Copilot plugin is installed if (!AppModPluginInstaller.isCopilotInstalled()) { From 383911ad71fd811e9f9cb92ba46f457c0856473d Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Mon, 1 Jun 2026 15:58:08 +0800 Subject: [PATCH 5/9] Readjustment for fix of issue 325. Now the `(Install GitHub Copilot Mordenization)` suffix doesn't unexpectedly repeat evertime hovering the level 1 menu "GitHub Copilot". --- .../action/CveFixDependencyInProblemsViewAction.java | 5 ++++- .../javaupgrade/action/JavaUpgradeContextMenuAction.java | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java index ca1fc05af1..04d776deb3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -93,8 +93,11 @@ public void update(@NotNull AnActionEvent e) { } e.getPresentation().setEnabledAndVisible(true); // e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + final String baseText = getTemplatePresentation().getText(); if (!AppModPluginInstaller.isAppModPluginInstalled()) { - e.getPresentation().setText(e.getPresentation().getText() + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + e.getPresentation().setText(baseText + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + } else { + e.getPresentation().setText(baseText); } } catch (Throwable ex) { // In case of any error, hide the action diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index 4f56c35018..c22103f974 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -56,8 +56,11 @@ public void update(@NotNull AnActionEvent e) { isMavenBuildFile(file) || isGradleBuildFile(file); } + final String baseText = getTemplatePresentation().getText(); if (!isAppModPluginInstalled()) { - e.getPresentation().setText(e.getPresentation().getText() + TO_INSTALL_APP_MODE_PLUGIN); + e.getPresentation().setText(baseText + TO_INSTALL_APP_MODE_PLUGIN); + } else { + e.getPresentation().setText(baseText); } if (visible){ AppModUtils.logTelemetryEvent("showJavaUpgradeContextMenuAction", Map.of("appmodPluginInstalled", String.valueOf(isAppModPluginInstalled()))); From 3c478deea27a35b936344e1b0e1e687a67852fbe Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Mon, 8 Jun 2026 10:27:47 +0800 Subject: [PATCH 6/9] Supplemented appmod prefix for tool validate_cves_for_java --- .../toolkit/intellij/appmod/javaupgrade/utils/Constants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java index 194d0e38e4..a08db8d550 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java @@ -4,7 +4,7 @@ public class Constants { public static final String UPGRADE_JAVA_AND_FRAMEWORK_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #appmod-generate-upgrade-plan"; public static final String UPGRADE_JAVA_VERSION_PROMPT = "Upgrade Java runtime from version %s to the latest LTS version using java upgrade tools by invoking #appmod-generate-upgrade-plan"; public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "Upgrade %s from version %s to the latest LTS version using java upgrade tools by invoking #appmod-generate-upgrade-plan"; - public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #appmod-validate_cves_for_java"; public static final String UPGRADE_JDK_WITH_COPILOT_DISPLAY_NAME = "Upgrade JDK with Copilot"; public static final String UPGRADE_SPRING_BOOT_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Boot with Copilot"; public static final String UPGRADE_SPRING_FRAMEWORK_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Framework with Copilot"; From d711b6b0bf1480d86b936f01affc79c4ef47fafa Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Mon, 8 Jun 2026 14:51:44 +0800 Subject: [PATCH 7/9] validate_cves_for_java -> appmod-validate-cves-for-java --- .../toolkit/intellij/appmod/javaupgrade/utils/Constants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java index a08db8d550..88bca59917 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java @@ -4,7 +4,7 @@ public class Constants { public static final String UPGRADE_JAVA_AND_FRAMEWORK_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #appmod-generate-upgrade-plan"; public static final String UPGRADE_JAVA_VERSION_PROMPT = "Upgrade Java runtime from version %s to the latest LTS version using java upgrade tools by invoking #appmod-generate-upgrade-plan"; public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "Upgrade %s from version %s to the latest LTS version using java upgrade tools by invoking #appmod-generate-upgrade-plan"; - public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #appmod-validate_cves_for_java"; + public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #appmod-validate-cves-for-java"; public static final String UPGRADE_JDK_WITH_COPILOT_DISPLAY_NAME = "Upgrade JDK with Copilot"; public static final String UPGRADE_SPRING_BOOT_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Boot with Copilot"; public static final String UPGRADE_SPRING_FRAMEWORK_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Framework with Copilot"; From 25d420a0e72248bc77ebec2f3cb0efe8c156db26 Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Wed, 17 Jun 2026 15:28:10 +0800 Subject: [PATCH 8/9] Fixed cve fixing relevant issues. 1. Fixed issue that CVE fixing doesn't use the given custom agent. 2. Fixed issue that "Fix the vulnerable with GitHub Copilot" button is missing in problems view -> problem right-click context menu. --- .../JavaUpgradeCheckStartupActivity.java | 31 ++ .../CveFixDependencyInProblemsViewAction.java | 14 +- .../CveFixDependencyIntentionAction.java | 2 +- .../action/CveFixInProblemsViewAction.java | 3 +- .../action/CveFixIntentionAction.java | 3 +- .../action/JavaUpgradeContextMenuAction.java | 3 +- .../action/JavaUpgradeQuickFix.java | 3 +- .../JavaVersionNotificationService.java | 303 +++++++++++++++++- .../appmod/javaupgrade/utils/Constants.java | 2 + 9 files changed, 346 insertions(+), 18 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java index 023d7c58db..b4e892ae43 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -10,6 +10,9 @@ import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.ProjectActivity; +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManagerCore; +import com.intellij.openapi.extensions.PluginId; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; @@ -32,6 +35,8 @@ public class JavaUpgradeCheckStartupActivity implements ProjectActivity, DumbAwa // Additional delay after smart mode to ensure Maven/Gradle sync is complete private static final long POST_INDEXING_DELAY_SECONDS = 3; + private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; + private static final String COPILOT_CHAT_MODE_SERVICE_CLASS = "com.github.copilot.agent.chatMode.ChatModeService"; @Override public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { @@ -67,6 +72,11 @@ private void performJavaUpgradeCheck(@Nonnull Project project) { if (project.isDisposed()) { return; } + + // Warm up Copilot's lazy chat-mode indexing once per project open. The first explicit + // access to ChatModeService.chatModes.value is what causes Copilot to discover custom + // agents from .github/agents/, so we trigger that before the user clicks the fix action. + warmUpCopilotChatModes(project); // Refresh the cache (this populates JDK and dependency issues for use by inspections) final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); @@ -101,4 +111,25 @@ private void performJavaUpgradeCheck(@Nonnull Project project) { log.error("Error performing Java upgrade check for project: {}", project.getName(), e); } } + + private void warmUpCopilotChatModes(@Nonnull Project project) { + try { + final IdeaPluginDescriptor copilot = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); + if (copilot == null || !copilot.isEnabled() || copilot.getPluginClassLoader() == null) { + return; + } + final Class chatModeServiceClass = copilot.getPluginClassLoader().loadClass(COPILOT_CHAT_MODE_SERVICE_CLASS); + final Object service = project.getService(chatModeServiceClass); + if (service == null) { + return; + } + final var method = service.getClass().getMethod("getChatModes"); + final Object flow = method.invoke(service); + final var getValue = flow.getClass().getMethod("getValue"); + getValue.invoke(flow); + } catch (Throwable e) { + // Best effort only; the fix action still falls back to its own URI path. + log.warn("Failed to warm up Copilot Chat modes: {}", project.getName(), e); + } + } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java index 04d776deb3..2dc05fbdbf 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -11,7 +11,6 @@ import com.intellij.openapi.vfs.VirtualFile; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.ProblemsViewUtils; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; @@ -47,13 +46,15 @@ public void actionPerformed(@NotNull AnActionEvent e) { if (vulnerabilityInfo == null) { JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( project, - SCAN_AND_RESOLVE_CVES_PROMPT + SCAN_AND_RESOLVE_CVES_PROMPT, + APPMOD_CVE_AGENT_NAME ); } else { JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( project, String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, - vulnerabilityInfo.getDependencyCoordinate()) + vulnerabilityInfo.getDependencyCoordinate()), + APPMOD_CVE_AGENT_NAME ); } AppModUtils.logTelemetryEvent("openCopilotChatForCveFixDependencyInProblemsViewAction", Map.of("appmodPluginInstalled", String.valueOf(AppModPluginInstaller.isAppModPluginInstalled()))); @@ -82,12 +83,7 @@ public void update(@NotNull AnActionEvent e) { final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); final boolean isBuildFile = isBuildFile(file); - if (!isBuildFile || !isCVEIssue(description)) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(vulnerabilityInfo.getGroupId() + ":" + vulnerabilityInfo.getArtifactId()); - if (issue == null) { + if (!isBuildFile || !isCVEIssue(description) || vulnerabilityInfo == null) { e.getPresentation().setEnabledAndVisible(false); return; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java index 8f3114202b..72d0f48222 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java @@ -103,7 +103,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws // Try to extract dependency information from the current context final String prompt = buildPromptFromContext(editor, file); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt, APPMOD_CVE_AGENT_NAME); AppModUtils.logTelemetryEvent("openCveFixDependencyCopilotChatFromIntentionAction"); } catch (Throwable e) { log.error("Failed to invoke CveFixDependencyIntentionAction: ", e); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java index fe05c38851..1c5efe6d96 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java @@ -46,7 +46,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( project, - SCAN_AND_RESOLVE_CVES_PROMPT + SCAN_AND_RESOLVE_CVES_PROMPT, + APPMOD_CVE_AGENT_NAME ); AppModUtils.logTelemetryEvent("openCopilotChatForCveFixInProblemsViewAction", Map.of("appmodPluginInstalled", String.valueOf(AppModPluginInstaller.isAppModPluginInstalled()))); } catch (Throwable ex) { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index 01fc11e809..4c7c4ec637 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -24,6 +24,7 @@ import java.util.Map; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.APPMOD_CVE_AGENT_NAME; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.SCAN_AND_RESOLVE_CVES_PROMPT; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME; @@ -107,7 +108,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws // Try to extract dependency information from the current context final String prompt = buildPromptFromContext(); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt, APPMOD_CVE_AGENT_NAME); AppModUtils.logTelemetryEvent("openCveFixCopilotChatFromIntentionAction", Map.of("AppModPluginInstalled", String.valueOf(AppModPluginInstaller.isAppModPluginInstalled()))); } catch (Throwable e) { log.error("Failed to invoke CveFixIntentionAction: ", e); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index c22103f974..c61814e4c7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -21,6 +21,7 @@ import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.APPMOD_UPGRADE_AGENT_NAME; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; /** @@ -84,7 +85,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { String prompt = buildUpgradePrompt(); // Open Copilot chat with the upgrade prompt - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt, APPMOD_UPGRADE_AGENT_NAME); AppModUtils.logTelemetryEvent("openJavaUpgradeCopilotChatFromContextMenu", Map.of("appmodPluginInstalled", String.valueOf(isAppModPluginInstalled()))); } catch (Throwable ex) { // Log error but do not crash diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java index ba63225d7d..fe265da1e3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -18,6 +18,7 @@ import java.util.Map; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.APPMOD_UPGRADE_AGENT_NAME; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.UPGRADE_JAVA_FRAMEWORK_PROMPT; /** @@ -55,7 +56,7 @@ public String getName() { public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { try { String prompt = buildPromptForIssue(issue); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt, APPMOD_UPGRADE_AGENT_NAME); AppModUtils.logTelemetryEvent("openCopilotChatForJavaUpgradeQuickFix", Map.of("appmodPluginInstalled", String.valueOf(AppModPluginInstaller.isAppModPluginInstalled()))); } catch (Throwable ex) { log.error("Failed to apply Java upgrade quick fix", ex); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index ffc8ee3dab..6a09509e51 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -14,6 +14,7 @@ import com.intellij.notification.Notifications; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; @@ -25,6 +26,11 @@ import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; import com.microsoft.azure.toolkit.lib.common.telemetry.AzureTelemeter; import kotlin.Unit; @@ -32,6 +38,7 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.List; import java.util.Map; import java.util.Objects; @@ -52,6 +59,14 @@ public class JavaVersionNotificationService { // GitHub Copilot plugin ID private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; + // App Modernization plugin (registers the custom agents we want to pre-select). + private static final String APPMOD_PLUGIN_ID = "com.github.copilot.appmod"; + // Resolved from the Copilot plugin via reflection (older versions don't expose it). + private static final String COPILOT_CHAT_MODE_SERVICE_CLASS = "com.github.copilot.agent.chatMode.ChatModeService"; + // Copilot indexes project-scope .github/agents/*.agent.md asynchronously after project open; + // the first action can miss the agent for quite a while, so retry off-EDT for up to ~20 s. + private static final int AGENT_RESOLVE_RETRY_ATTEMPTS = 100; + private static final long AGENT_RESOLVE_RETRY_DELAY_MS = 200L; private static JavaVersionNotificationService instance; @@ -293,7 +308,7 @@ private boolean isUpgradeSupported(@Nonnull JavaUpgradeIssue issue) { */ private void openCopilotChatWithUpgradePrompt(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue) { final String prompt = buildUpgradePrompt(issue); - openCopilotChatWithPrompt(project, prompt); + openCopilotChatWithPrompt(project, prompt, APPMOD_UPGRADE_AGENT_NAME); } /** @@ -317,7 +332,7 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String // } // Fallback to reflection for cross-version compatibility - if (tryReflectionCopilotCall(project, prompt)) { + if (tryReflectionCopilotCall(project, prompt, null, null)) { return; // Success via reflection } @@ -331,6 +346,74 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String } } + /** + * Same as {@link #openCopilotChatWithPrompt(Project, String)} but pre-selects a Copilot custom + * chat-mode (agent) by name (e.g. {@code "modernize-java-security"}). The URI is resolved on a + * pooled background thread (with a short bounded retry to absorb Copilot's lazy {@code + * .github/agents/} indexing on first project open) to keep the EDT responsive. When the agent + * is not registered (older Copilot, appmod plugin not installed, etc.) we silently fall back + * to plain Agent Mode. + */ + public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String prompt, + @Nullable String customAgentName) { + if (customAgentName == null || customAgentName.isBlank()) { + openCopilotChatWithPrompt(project, prompt); + return; + } + // Capture this once so users without the appmod plugin don't pay the retry cost — we'll just + // prompt to install on the EDT below. + final boolean appmodInstalled = isAppModPluginInstalled(); + ApplicationManager.getApplication().executeOnPooledThread(() -> { + Object uri = null; + if (appmodInstalled) { + final IdeaPluginDescriptor copilot = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); + if (copilot != null && copilot.isEnabled()) { + final ClassLoader cl = copilot.getPluginClassLoader(); + if (cl != null) { + for (int i = 0; i < AGENT_RESOLVE_RETRY_ATTEMPTS && !project.isDisposed(); i++) { + uri = resolveCustomAgentUri(project, cl, customAgentName); + if (uri != null) { + break; + } + try { + Thread.sleep(AGENT_RESOLVE_RETRY_DELAY_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + if (uri == null && !project.isDisposed()) { + log.info("openCopilotChatWithPrompt: agent '{}' not in ChatModeService after {} attempts ({} ms); will try file-based fallback.", + customAgentName, AGENT_RESOLVE_RETRY_ATTEMPTS, AGENT_RESOLVE_RETRY_ATTEMPTS * AGENT_RESOLVE_RETRY_DELAY_MS); + } + } + } + // Fallback: ChatModeService is populated lazily by Copilot only after the first + // chat-panel open (~17 s after triggering query()), so the first click after project + // open will miss the agent via that path. Bypass it by pointing withAgentMode(...) + // directly at the .agent.md file on disk; Copilot loads the definition on demand. + if (uri == null && !project.isDisposed()) { + uri = tryConstructFileBasedAgentUri(project, customAgentName); + if (uri != null) { + log.info("openCopilotChatWithPrompt: ChatModeService didn't expose '{}'; using file-based fallback uri={}", customAgentName, uri); + } + } + } + final Object preResolvedAgentUri = uri; + AzureTaskManager.getInstance().runLater(() -> { + if (!appmodInstalled) { + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project, true)); + return; + } + if (tryReflectionCopilotCall(project, prompt, customAgentName, preResolvedAgentUri)) { + return; + } + log.info("Failed to open Copilot chat via both direct and reflection methods."); + showGenericUpgradeGuidance(project, prompt); + }); + }); + } + /** * Tries to call CopilotChatService directly (works when compile-time and runtime versions match). * @return true if successful, false if an error occurred @@ -358,9 +441,14 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String /** * Tries to call CopilotChatService via reflection for cross-version compatibility. + * @param customAgentName optional name of the Copilot custom chat-mode to select; null for default Agent Mode + * @param preResolvedAgentUri optional URI already resolved off-EDT for {@code customAgentName}; + * when non-null, the lookup against ChatModeService is skipped * @return true if successful, false if an error occurred */ - private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull String prompt) { + private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull String prompt, + @Nullable String customAgentName, + @Nullable Object preResolvedAgentUri) { try { // Get the Copilot plugin's classloader to load its classes final IdeaPluginDescriptor copilotPlugin = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); @@ -388,7 +476,6 @@ private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull Stri Function1 queryBuilder = builder -> { try { builder.getClass().getMethod("withInput", String.class).invoke(builder, prompt); - builder.getClass().getMethod("withAgentMode").invoke(builder); builder.getClass().getMethod("withNewSession").invoke(builder); withModelCompatibility(builder, DEFAULT_MODEL_NAME); Method withSessionIdReceiverMethod = findMethodByName(builder.getClass(), "withSessionIdReceiver"); @@ -396,6 +483,7 @@ private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull Stri Function1 sessionIdReceiver = sessionId -> Unit.INSTANCE; withSessionIdReceiverMethod.invoke(builder, sessionIdReceiver); } + applyAgentMode(builder, project, copilotClassLoader, customAgentName, preResolvedAgentUri); } catch (Exception ex) { // Error configuring query builder via reflection log.error("Error configuring Copilot query via reflection: " + ex.getMessage()); @@ -413,6 +501,213 @@ private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull Stri return false; } + /** + * Switches the builder into Agent Mode, optionally selecting a custom agent by URI. Mirrors: + *
{@code
+     * val uri = project.service().chatModes.value.firstOrNull { it.name == name }?.uri
+     * if (uri != null) withAgentMode(uri) else withAgentMode()
+     * }
+ * Falls back to no-arg {@code withAgentMode()} on any failure so the chat still opens. + */ + private static void applyAgentMode(@Nonnull Object builder, @Nonnull Project project, + @Nonnull ClassLoader copilotClassLoader, + @Nullable String customAgentName, + @Nullable Object preResolvedAgentUri) { + Object agentUri = preResolvedAgentUri; + if (agentUri == null && customAgentName != null && !customAgentName.isBlank()) { + // Caller didn't pre-resolve (or the off-EDT lookup returned null); try once here. + agentUri = resolveCustomAgentUri(project, copilotClassLoader, customAgentName); + } + try { + if (agentUri != null) { + final Method withAgentMode = findAccessibleMethod(builder.getClass(), "withAgentMode", 0); + if (withAgentMode != null) { + withAgentMode.invoke(builder); + } + final Method withAgentModeUri = findAccessibleMethod(builder.getClass(), "withAgentMode", 1); + if (withAgentModeUri != null) { + // Copilot may declare withAgentMode as taking URI or String depending on version; + // coerce our agent URI to whichever the installed plugin expects. + final Object coerced = coerceToParameterType(agentUri, withAgentModeUri.getParameterTypes()[0]); + if (coerced != null) { + withAgentModeUri.invoke(builder, coerced); + log.info("applyAgentMode: selected Copilot custom agent '{}' via withAgentMode(uri) — uri={}", customAgentName, agentUri); + return; + } + log.warn("Resolved Copilot agent '{}' uri={} but cannot coerce to withAgentMode parameter type {}; using default Agent Mode.", + customAgentName, agentUri, withAgentModeUri.getParameterTypes()[0].getName()); + } else { + log.warn("Resolved Copilot agent '{}' but withAgentMode(uri) is not exposed by this Copilot version; using default Agent Mode.", + customAgentName); + } + } else if (customAgentName != null && !customAgentName.isBlank()) { + log.info("applyAgentMode: Copilot custom agent '{}' was not resolvable (not in chatModes and no on-disk file found); falling back to default Agent Mode.", + customAgentName); + } + } catch (Exception ex) { + log.warn("Failed to apply Agent Mode via reflection: " + ex.getMessage(), ex); + } + } + + /** + * Probes the well-known on-disk locations of {@code .agent.md} and returns a {@link java.net.URI} + * if found. Used as a fallback when {@link #resolveCustomAgentUri} can't see the agent yet because + * Copilot's {@code ChatModeService} populates the list lazily on first chat-panel open. + * + *

Search order: + *

    + *
  1. Project-scope: {@code /.github/agents/.agent.md}
  2. + *
  3. Plugin-scope: {@code /mcp-server/dist/entrypoints/agents/.agent.md}
  4. + *
+ */ + @Nullable + private static java.net.URI tryConstructFileBasedAgentUri(@Nonnull Project project, @Nonnull String customAgentName) { + try { + final String basePath = project.getBasePath(); + if (basePath != null) { + final java.nio.file.Path projectFile = java.nio.file.Paths.get( + basePath, ".github", "agents", customAgentName + ".agent.md"); + if (java.nio.file.Files.isRegularFile(projectFile)) { + return toCopilotUri(projectFile); + } + } + final IdeaPluginDescriptor appmod = PluginManagerCore.getPlugin(PluginId.getId(APPMOD_PLUGIN_ID)); + if (appmod != null && appmod.getPluginPath() != null) { + final java.nio.file.Path pluginFile = appmod.getPluginPath() + .resolve(java.nio.file.Paths.get("mcp-server", "dist", "entrypoints", "agents", + customAgentName + ".agent.md")); + if (java.nio.file.Files.isRegularFile(pluginFile)) { + return toCopilotUri(pluginFile); + } + } + } catch (Exception ex) { + log.warn("tryConstructFileBasedAgentUri failed for '" + customAgentName + "': " + ex.getMessage()); + } + return null; + } + + /** + * Returns a {@link java.net.URI} for {@code path} in the exact form Copilot uses for its chat-mode + * registry (VS Code convention: lowercase drive letter, percent-encoded colon on Windows). Copilot + * does string-equality matching against this registry; a Java-standard {@code file:///C:/...} URI + * is silently ignored even when it points to the same file. Non-Windows paths are returned as-is. + */ + @Nonnull + private static java.net.URI toCopilotUri(@Nonnull java.nio.file.Path path) throws java.net.URISyntaxException { + final java.net.URI standard = path.toUri(); + final String s = standard.toString(); + // Match file:///:/... and rewrite to file:///%3A/... + if (s.length() >= 11 && s.startsWith("file:///") && s.charAt(9) == ':' && Character.isLetter(s.charAt(8))) { + return new java.net.URI("file:///" + Character.toLowerCase(s.charAt(8)) + "%3A" + s.substring(10)); + } + return standard; + } + + /** + * Best-effort coercion of {@code value} into an instance of {@code paramType}. Returns {@code null} + * when no safe conversion is possible. Handles the cases that actually occur in practice for + * Copilot's {@code withAgentMode(...)}: {@link java.net.URI} ↔ {@link String}. + */ + @Nullable + private static Object coerceToParameterType(@Nullable Object value, @Nonnull Class paramType) { + if (value == null) { + return null; + } + if (paramType.isInstance(value)) { + return value; + } + if (paramType == String.class) { + return value.toString(); + } + if (paramType == java.net.URI.class && value instanceof String) { + try { + return new java.net.URI((String) value); + } catch (Exception ignored) { + return null; + } + } + return null; + } + + + /** + * Reflectively resolves {@code chatModes.value.firstOrNull { it.name == name }?.uri} on + * Copilot's {@code ChatModeService}. Returns {@code null} when the service is missing + * (older Copilot), the agent is not yet registered, or any reflection step fails. + */ + @Nullable + private static Object resolveCustomAgentUri(@Nonnull Project project, + @Nonnull ClassLoader copilotClassLoader, + @Nonnull String customAgentName) { + try { + final Class chatModeServiceClass = copilotClassLoader.loadClass(COPILOT_CHAT_MODE_SERVICE_CLASS); + final Object chatModeService = project.getService(chatModeServiceClass); + if (chatModeService == null) { + return null; + } + final Method getChatModes = findAccessibleMethod(chatModeService.getClass(), "getChatModes", 0); + if (getChatModes == null) { + return null; + } + final Object flow = getChatModes.invoke(chatModeService); + if (flow == null) { + return null; + } + // StateFlow#getValue() may live on a package-private subclass (e.g. DerivedStateFlow); + // findAccessibleMethod walks up to the public StateFlow interface. + final Method getValue = findAccessibleMethod(flow.getClass(), "getValue", 0); + final Object modes = getValue != null ? getValue.invoke(flow) : flow; + if (!(modes instanceof Iterable)) { + return null; + } + for (Object mode : (Iterable) modes) { + if (mode == null) continue; + final Method getName = findAccessibleMethod(mode.getClass(), "getName", 0); + if (getName == null) continue; + if (customAgentName.equals(String.valueOf(getName.invoke(mode)))) { + final Method getUri = findAccessibleMethod(mode.getClass(), "getUri", 0); + return getUri == null ? null : getUri.invoke(mode); + } + } + } catch (ClassNotFoundException ex) { + // Older Copilot without ChatModeService; caller will fall back to plain Agent Mode. + } catch (Exception ex) { + log.warn("Failed to resolve Copilot custom agent '" + customAgentName + "': " + ex.getMessage()); + } + return null; + } + + /** + * Returns a publicly-invokable {@link Method} matching {@code name} and exact {@code parameterCount}. + * Walks the runtime class, its superclasses and all interfaces (BFS) and returns the first match + * whose declaring class is public — required because some Copilot return types are + * package-private (e.g. Kotlin's {@code DerivedStateFlow}) and {@link Method#invoke} on a method + * whose declaring class is non-public throws {@link IllegalAccessException}. + */ + @Nullable + private static Method findAccessibleMethod(@Nonnull Class clazz, @Nonnull String name, int parameterCount) { + final Deque> queue = new ArrayDeque<>(); + final Set> seen = new HashSet<>(); + queue.add(clazz); + while (!queue.isEmpty()) { + final Class c = queue.poll(); + if (c == null || !seen.add(c)) continue; + if (Modifier.isPublic(c.getModifiers())) { + for (Method m : c.getDeclaredMethods()) { + if (!m.isSynthetic() + && name.equals(m.getName()) + && m.getParameterCount() == parameterCount + && Modifier.isPublic(m.getModifiers())) { + return m; + } + } + } + if (c.getSuperclass() != null) queue.add(c.getSuperclass()); + for (Class iface : c.getInterfaces()) queue.add(iface); + } + return null; + } + /** * Shows generic guidance for upgrading when Copilot is not available. * @param project The project context diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java index 88bca59917..e8a6c0bc8a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java @@ -13,4 +13,6 @@ public class Constants { public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT = "Fix the vulnerable dependency %s by using #appmod-validate-cves-for-java"; public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME = "Fix the vulnerable dependency with Copilot"; public static final String ISSUE_DISPLAY_NAME = "Your project uses %s %s. Consider upgrading to %s to the latest LTS version for better performance and support"; + public static final String APPMOD_CVE_AGENT_NAME = "modernize-java-security"; + public static final String APPMOD_UPGRADE_AGENT_NAME = "modernize-java-upgrade"; } From 1389f713f862eb312ca5981be473663975fb8a0c Mon Sep 17 00:00:00 2001 From: Ruiming <15029672963@163.com> Date: Wed, 24 Jun 2026 15:30:58 +0800 Subject: [PATCH 9/9] Readjustments for code review. --- .../JavaUpgradeCheckStartupActivity.java | 21 ++--- .../JavaVersionNotificationService.java | 83 +++++++++++++++++-- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java index b4e892ae43..d5b2a73a7d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -36,7 +36,6 @@ public class JavaUpgradeCheckStartupActivity implements ProjectActivity, DumbAwa // Additional delay after smart mode to ensure Maven/Gradle sync is complete private static final long POST_INDEXING_DELAY_SECONDS = 3; private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; - private static final String COPILOT_CHAT_MODE_SERVICE_CLASS = "com.github.copilot.agent.chatMode.ChatModeService"; @Override public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { @@ -73,9 +72,10 @@ private void performJavaUpgradeCheck(@Nonnull Project project) { return; } - // Warm up Copilot's lazy chat-mode indexing once per project open. The first explicit - // access to ChatModeService.chatModes.value is what causes Copilot to discover custom - // agents from .github/agents/, so we trigger that before the user clicks the fix action. + // Warm up Copilot's lazy chat-mode indexing once per project open. Copilot only scans + // custom agents from .github/agents/ when its chat-mode registry is explicitly refreshed + // (otherwise not until the chat panel is first opened), so we trigger that refresh now — + // before the user clicks a fix action — so the agent is resolvable on the very first click. warmUpCopilotChatModes(project); // Refresh the cache (this populates JDK and dependency issues for use by inspections) @@ -118,15 +118,10 @@ private void warmUpCopilotChatModes(@Nonnull Project project) { if (copilot == null || !copilot.isEnabled() || copilot.getPluginClassLoader() == null) { return; } - final Class chatModeServiceClass = copilot.getPluginClassLoader().loadClass(COPILOT_CHAT_MODE_SERVICE_CLASS); - final Object service = project.getService(chatModeServiceClass); - if (service == null) { - return; - } - final var method = service.getClass().getMethod("getChatModes"); - final Object flow = method.invoke(service); - final var getValue = flow.getClass().getMethod("getValue"); - getValue.invoke(flow); + // Actively trigger Copilot to (re)scan custom agents so its chat-mode registry is populated + // before the first fix-action click. Merely reading the chatModes StateFlow does NOT populate + // it — only refreshChatModes() does — which is why a cold first click previously missed the agent. + JavaVersionNotificationService.triggerChatModesRefresh(project, copilot.getPluginClassLoader()); } catch (Throwable e) { // Best effort only; the fix action still falls back to its own URI path. log.warn("Failed to warm up Copilot Chat modes: {}", project.getName(), e); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index 6a09509e51..437c933a54 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -42,6 +42,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; /** * Service to display notifications about outdated Java project versions. @@ -67,6 +68,13 @@ public class JavaVersionNotificationService { // the first action can miss the agent for quite a while, so retry off-EDT for up to ~20 s. private static final int AGENT_RESOLVE_RETRY_ATTEMPTS = 100; private static final long AGENT_RESOLVE_RETRY_DELAY_MS = 200L; + + // Successfully resolved agent URIs, keyed by "|". Once an agent + // is resolved we never need to pay the retry cost again for that project, so repeated clicks are instant. + private static final Map RESOLVED_AGENT_URIS = new ConcurrentHashMap<>(); + // Keys with an in-flight resolution (same key format). Lets rapid repeat clicks share/skip a single + // ~20 s retry loop instead of each spawning its own sleeping thread. + private static final Set IN_FLIGHT_AGENT_RESOLUTIONS = ConcurrentHashMap.newKeySet(); private static JavaVersionNotificationService instance; @@ -363,13 +371,24 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String // Capture this once so users without the appmod plugin don't pay the retry cost — we'll just // prompt to install on the EDT below. final boolean appmodInstalled = isAppModPluginInstalled(); + // De-dup in-flight resolution: while one pooled thread is already resolving this agent, drop + // rapid repeat clicks instead of each spawning its own ~20 s sleeping thread. + final String cacheKey = project.getLocationHash() + "|" + customAgentName; + if (appmodInstalled && RESOLVED_AGENT_URIS.get(cacheKey) == null && !IN_FLIGHT_AGENT_RESOLUTIONS.add(cacheKey)) { + return; + } ApplicationManager.getApplication().executeOnPooledThread(() -> { - Object uri = null; - if (appmodInstalled) { + Object uri = appmodInstalled ? RESOLVED_AGENT_URIS.get(cacheKey) : null; + if (appmodInstalled && uri == null) { final IdeaPluginDescriptor copilot = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); if (copilot != null && copilot.isEnabled()) { final ClassLoader cl = copilot.getPluginClassLoader(); if (cl != null) { + // Copilot populates its chat-mode registry lazily (otherwise only after the chat + // panel is first opened). Without this active trigger the very first click finds an + // empty registry, the agent URI isn't honored, and the chat opens in default Agent + // Mode — so kick a refresh, then poll until the agent shows up. + triggerChatModesRefresh(project, cl); for (int i = 0; i < AGENT_RESOLVE_RETRY_ATTEMPTS && !project.isDisposed(); i++) { uri = resolveCustomAgentUri(project, cl, customAgentName); if (uri != null) { @@ -398,6 +417,13 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String log.info("openCopilotChatWithPrompt: ChatModeService didn't expose '{}'; using file-based fallback uri={}", customAgentName, uri); } } + if (uri != null) { + // Cache so subsequent clicks open instantly without re-running the retry loop. + RESOLVED_AGENT_URIS.put(cacheKey, uri); + } + } + if (appmodInstalled) { + IN_FLIGHT_AGENT_RESOLUTIONS.remove(cacheKey); } final Object preResolvedAgentUri = uri; AzureTaskManager.getInstance().runLater(() -> { @@ -540,9 +566,17 @@ private static void applyAgentMode(@Nonnull Object builder, @Nonnull Project pro log.warn("Resolved Copilot agent '{}' but withAgentMode(uri) is not exposed by this Copilot version; using default Agent Mode.", customAgentName); } - } else if (customAgentName != null && !customAgentName.isBlank()) { - log.info("applyAgentMode: Copilot custom agent '{}' was not resolvable (not in chatModes and no on-disk file found); falling back to default Agent Mode.", - customAgentName); + } else { + // agentUri == null: fallback to default Agent Mode + if (customAgentName != null && !customAgentName.isBlank()) { + log.info("applyAgentMode: Copilot custom agent '{}' was not resolvable (not in chatModes and no on-disk file found); falling back to default Agent Mode.", + customAgentName); + } + final Method withAgentMode = findAccessibleMethod(builder.getClass(), "withAgentMode", 0); + if (withAgentMode != null) { + withAgentMode.invoke(builder); + log.info("applyAgentMode: activated default Agent Mode"); + } } } catch (Exception ex) { log.warn("Failed to apply Agent Mode via reflection: " + ex.getMessage(), ex); @@ -591,6 +625,13 @@ private static java.net.URI tryConstructFileBasedAgentUri(@Nonnull Project proje * registry (VS Code convention: lowercase drive letter, percent-encoded colon on Windows). Copilot * does string-equality matching against this registry; a Java-standard {@code file:///C:/...} URI * is silently ignored even when it points to the same file. Non-Windows paths are returned as-is. + * + *

Limitation: only local drive-letter paths ({@code C:\...}) are rewritten. A Windows UNC path + * ({@code \\server\share\...}) maps to {@code file://server/share/...} (no drive letter), doesn't + * match the rewrite shape, and is returned as the Java-standard URI. If Copilot stored that agent + * under a different UNC spelling, the string-equality match would miss and we'd fall back to plain + * Agent Mode. This only affects projects/plugins hosted on a network share and is left unhandled + * because Copilot's canonical UNC form isn't verifiable here; the file-based fallback simply no-ops. */ @Nonnull private static java.net.URI toCopilotUri(@Nonnull java.nio.file.Path path) throws java.net.URISyntaxException { @@ -600,6 +641,7 @@ private static java.net.URI toCopilotUri(@Nonnull java.nio.file.Path path) throw if (s.length() >= 11 && s.startsWith("file:///") && s.charAt(9) == ':' && Character.isLetter(s.charAt(8))) { return new java.net.URI("file:///" + Character.toLowerCase(s.charAt(8)) + "%3A" + s.substring(10)); } + // Non-Windows paths and Windows UNC paths (file://server/share/...) fall through unchanged; see Javadoc. return standard; } @@ -630,6 +672,35 @@ private static Object coerceToParameterType(@Nullable Object value, @Nonnull Cla } + /** + * Reflectively calls {@code ChatModeService.refreshChatModes()} to make Copilot (re)scan custom + * agents from {@code .github/agents/} and plugin-provided locations. Copilot populates its chat-mode + * registry lazily — otherwise only after the chat panel is first opened — so without this trigger the + * very first fix-action click finds an empty registry, the resolved agent URI isn't honored, and the + * chat falls back to default Agent Mode. The refresh is asynchronous; callers poll {@code getChatModes} + * afterward. Best-effort: older Copilot builds may not expose the method, in which case this no-ops. + */ + public static void triggerChatModesRefresh(@Nonnull Project project, @Nonnull ClassLoader copilotClassLoader) { + try { + final Class chatModeServiceClass = copilotClassLoader.loadClass(COPILOT_CHAT_MODE_SERVICE_CLASS); + final Object chatModeService = project.getService(chatModeServiceClass); + if (chatModeService == null) { + return; + } + final Method refresh = findAccessibleMethod(chatModeService.getClass(), "refreshChatModes", 0); + if (refresh != null) { + refresh.invoke(chatModeService); + } else { + log.info("triggerChatModesRefresh: refreshChatModes() not exposed by this Copilot version."); + } + } catch (ClassNotFoundException ex) { + // Older Copilot without ChatModeService; nothing to refresh. + log.info("Older Copilot without ChatModeService"); + } catch (Exception ex) { + log.info("triggerChatModesRefresh failed: " + ex.getMessage()); + } + } + /** * Reflectively resolves {@code chatModes.value.firstOrNull { it.name == name }?.uri} on * Copilot's {@code ChatModeService}. Returns {@code null} when the service is missing @@ -685,7 +756,7 @@ private static Object resolveCustomAgentUri(@Nonnull Project project, * whose declaring class is non-public throws {@link IllegalAccessException}. */ @Nullable - private static Method findAccessibleMethod(@Nonnull Class clazz, @Nonnull String name, int parameterCount) { + public static Method findAccessibleMethod(@Nonnull Class clazz, @Nonnull String name, int parameterCount) { final Deque> queue = new ArrayDeque<>(); final Set> seen = new HashSet<>(); queue.add(clazz);