From 93211a5418f12268e062e06f9650a71e950c6430 Mon Sep 17 00:00:00 2001 From: haozhang Date: Tue, 28 Jul 2026 16:57:22 +0800 Subject: [PATCH 1/2] route appmod prompt to local agent --- .../service/CopilotLocalSessionHelper.java | 330 ++++++++++++++++++ .../JavaVersionNotificationService.java | 50 ++- 2 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java new file mode 100644 index 0000000000..6c4688f648 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManagerCore; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.project.Project; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.UUID; + +/** + * Creates a fresh LOCAL chat session before sending a prompt via {@code CopilotChatService.query()}, + * ensuring prompts always route to the Local agent regardless of the user's selected provider. + * + *

On Copilot 1.11+, {@code query(withNewSession())} reuses the home session whose target follows + * the user's provider selection (BACKGROUND/CLOUD/CLAUDE). If the user has selected a non-Local + * provider, the prompt lands on the wrong agent. This helper dispatches + * {@code ChatAction.CreateSession(newId, PANEL, LOCAL, agentMode, null, true)} before the query, + * creating a race-free LOCAL session. The caller uses {@code withExistingSession(newId)} instead of + * {@code withNewSession()}. + * + *

On older Copilot (1.9/1.10) where {@code CreateSession} doesn't exist, this falls back to + * switching the provider to LOCAL via {@code TargetSelected} or the provider service, and returns + * null so the caller uses {@code withNewSession()}. + * + *

All Copilot types are loaded through the Copilot plugin's classloader (since this plugin + * is not in the same classloader hierarchy) and accessed reflectively so a single binary works + * across Copilot versions. + */ +public final class CopilotLocalSessionHelper { + + private static final Logger LOG = Logger.getInstance(CopilotLocalSessionHelper.class); + + private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; + + private static final String FQ_CHAT_SERVICE = "com.github.copilot.session.ChatService"; + private static final String FQ_CHAT_TYPE = "com.github.copilot.session.Chat$Type"; + private static final String FQ_CHAT_TARGET_TYPE = "com.github.copilot.session.ChatTarget$Type"; + private static final String FQ_CREATE_SESSION = "com.github.copilot.session.ChatAction$CreateSession"; + private static final String FQ_TARGET_SELECTED = "com.github.copilot.session.ChatAction$TargetSelected"; + private static final String FQ_USER_SELECTED_PROVIDER = "com.github.copilot.agent.agentProvider.UserSelectedAgentProviderService"; + private static final String FQ_AGENT_PROVIDER = "com.github.copilot.agent.agentProvider.AgentProvider"; + private static final String FQ_CHATMODE_BUILTINS = "com.github.copilot.agent.chatMode.ChatModeService$BuiltInChatModes"; + private static final String DEFAULT_CTOR_MARKER = "kotlin.jvm.internal.DefaultConstructorMarker"; + + private static final String TARGET_LOCAL = "LOCAL"; + + private CopilotLocalSessionHelper() {} + + /** + * Prepares a fresh LOCAL session for the caller's upcoming {@code CopilotChatService.query()}. + * + * @param project the current project + * @return a fresh LOCAL session ID for {@code withExistingSession(id)}, or {@code null} when + * the caller should fall back to {@code withNewSession()}. + */ + @Nullable + public static String prepareLocalFallbackSession(@Nonnull Project project) { + ClassLoader cl = getCopilotClassLoader(); + if (cl == null) { + LOG.debug("prepareLocalFallbackSession: Copilot classloader unavailable."); + return null; + } + try { + String sessionId = createFreshLocalSessionId(project, cl); + if (sessionId != null) { + return sessionId; + } + } catch (Throwable t) { + LOG.debug("Could not create a fresh LOCAL session; falling back to withNewSession.", t); + } + // 1.9/1.10 or CreateSession unavailable: try switching provider to LOCAL synchronously. + try { + ensureLocalProvider(project, cl); + } catch (Throwable t) { + LOG.debug("Failed to switch provider to Local; continuing.", t); + } + return null; + } + + @Nullable + private static ClassLoader getCopilotClassLoader() { + IdeaPluginDescriptor copilot = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); + if (copilot == null || !copilot.isEnabled()) return null; + return copilot.getPluginClassLoader(); + } + + /** + * 1.11+ only: dispatches {@code ChatAction.CreateSession(newId, PANEL, LOCAL, agentMode, null, true)} + * and returns newId. Returns null when CreateSession is absent (1.9/1.10). + */ + @Nullable + private static String createFreshLocalSessionId(@Nonnull Project project, @Nonnull ClassLoader cl) { + if (classOrNull(cl, FQ_CREATE_SESSION) == null) return null; + Object chatService = projectService(project, cl, FQ_CHAT_SERVICE); + if (chatService == null) return null; + + String sessionId = UUID.randomUUID().toString(); + Object panel = enumConst(cl, FQ_CHAT_TYPE, "PANEL"); + Object local = enumConst(cl, FQ_CHAT_TARGET_TYPE, TARGET_LOCAL); + String agentModeId = builtinAgentModeId(cl); + + Object createAction = constructPadded(cl, FQ_CREATE_SESSION, + new Object[]{sessionId, panel, local, agentModeId, null, true}); + dispatch(chatService, createAction); + LOG.info("Created fresh LOCAL session " + sessionId + " for appmod fallback send."); + return sessionId; + } + + /** + * Switches the provider to LOCAL. Tries ChatAction.TargetSelected (1.11+) first, then the + * UserSelectedAgentProviderService (1.9/1.10). + */ + private static void ensureLocalProvider(@Nonnull Project project, @Nonnull ClassLoader cl) { + if (trySwitchViaTargetSelected(project, cl)) { + LOG.info("Switched provider -> LOCAL via ChatAction.TargetSelected"); + return; + } + if (trySwitchViaProviderService(project, cl)) { + LOG.info("Switched provider -> LOCAL via UserSelectedAgentProviderService"); + } + } + + private static boolean trySwitchViaTargetSelected(@Nonnull Project project, @Nonnull ClassLoader cl) { + try { + Object chatService = projectService(project, cl, FQ_CHAT_SERVICE); + if (chatService == null) return false; + Object chatFlow = invoke0OrNull(chatService, "getChat"); + if (chatFlow == null) chatFlow = invoke0OrNull(chatService, "getPanelChat"); + if (chatFlow == null) return false; + Object chat = invoke0OrNull(chatFlow, "getValue"); + if (chat == null) return false; + Object activeId = invoke0OrNull(chat, "getActiveSessionId"); + if (!(activeId instanceof String) || ((String) activeId).isEmpty()) return false; + Object localTarget = enumConst(cl, FQ_CHAT_TARGET_TYPE, TARGET_LOCAL); + if (localTarget == null) return false; + Object action = constructByArity(cl, FQ_TARGET_SELECTED, 4, + new Object[]{activeId, localTarget, null, null}); + dispatch(chatService, action); + return true; + } catch (Throwable t) { + LOG.debug("Provider switch via ChatAction.TargetSelected failed.", t); + return false; + } + } + + private static boolean trySwitchViaProviderService(@Nonnull Project project, @Nonnull ClassLoader cl) { + try { + Object providerService = projectService(project, cl, FQ_USER_SELECTED_PROVIDER); + if (providerService == null) return false; + Object local = enumConst(cl, FQ_AGENT_PROVIDER, TARGET_LOCAL); + if (local == null) return false; + Method setter = null; + for (Method m : providerService.getClass().getMethods()) { + if ("setSelectedAgentProvider".equals(m.getName()) && m.getParameterCount() == 1) { + setter = m; + break; + } + } + if (setter == null) return false; + setter.setAccessible(true); + setter.invoke(providerService, local); + return true; + } catch (Throwable t) { + LOG.debug("Provider switch via UserSelectedAgentProviderService failed.", t); + return false; + } + } + + // ── Reflection utilities (all use Copilot's classloader) ───────────────── + + @Nullable + private static String builtinAgentModeId(@Nonnull ClassLoader cl) { + try { + Object builtins = staticField(cl, FQ_CHATMODE_BUILTINS, "INSTANCE"); + if (builtins == null) return null; + Object agentMode = invoke0OrNull(builtins, "getAgent"); + if (agentMode == null) return null; + Object id = invoke0OrNull(agentMode, "getId"); + return id instanceof String ? (String) id : null; + } catch (Throwable t) { + LOG.debug("Could not resolve builtin agent mode id.", t); + return null; + } + } + + private static Object constructPadded(@Nonnull ClassLoader cl, String fqcn, Object[] base) { + try { + Class clazz = cl.loadClass(fqcn); + Constructor ctor = canonicalConstructor(clazz); + if (ctor == null) throw new IllegalStateException("No usable constructor on " + fqcn); + ctor.setAccessible(true); + return ctor.newInstance(padArgs(ctor, base)); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Failed to construct " + fqcn, e); + } + } + + private static Object constructByArity(@Nonnull ClassLoader cl, String fqcn, int arity, Object[] args) { + try { + Class clazz = cl.loadClass(fqcn); + for (Constructor c : clazz.getDeclaredConstructors()) { + if (c.getParameterCount() == arity) { + Class[] paramTypes = c.getParameterTypes(); + if (paramTypes.length > 0 && DEFAULT_CTOR_MARKER.equals(paramTypes[paramTypes.length - 1].getName())) { + continue; + } + c.setAccessible(true); + return c.newInstance(args); + } + } + throw new IllegalStateException("No " + arity + "-arg constructor on " + fqcn); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Failed to construct " + fqcn, e); + } + } + + private static void dispatch(Object store, Object action) { + try { + Method method = null; + for (Method m : store.getClass().getMethods()) { + if ("dispatch".equals(m.getName()) && m.getParameterCount() == 1) { + method = m; + break; + } + } + if (method == null) throw new IllegalStateException("No dispatch(Action) on " + store.getClass().getName()); + method.setAccessible(true); + method.invoke(store, action); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("dispatch failed", e); + } + } + + @Nullable + private static Constructor canonicalConstructor(Class clazz) { + Constructor best = null; + for (Constructor c : clazz.getDeclaredConstructors()) { + Class[] paramTypes = c.getParameterTypes(); + if (paramTypes.length == 0) continue; + if (DEFAULT_CTOR_MARKER.equals(paramTypes[paramTypes.length - 1].getName())) continue; + if (best == null || c.getParameterCount() > best.getParameterCount()) { + best = c; + } + } + return best; + } + + private static Object[] padArgs(Constructor ctor, Object[] base) { + Class[] types = ctor.getParameterTypes(); + Object[] out = new Object[types.length]; + System.arraycopy(base, 0, out, 0, base.length); + for (int i = base.length; i < types.length; i++) { + out[i] = defaultForType(types[i]); + } + return out; + } + + private static Object defaultForType(Class type) { + if (!type.isPrimitive()) return null; + if (type == boolean.class) return false; + if (type == char.class) return '\0'; + if (type == long.class) return 0L; + if (type == float.class) return 0.0f; + if (type == double.class) return 0.0; + return 0; // int, byte, short + } + + @Nullable + private static Class classOrNull(@Nonnull ClassLoader cl, String fqcn) { + try { + return cl.loadClass(fqcn); + } catch (Throwable t) { + return null; + } + } + + @Nullable + private static Object projectService(@Nonnull Project project, @Nonnull ClassLoader cl, String fqcn) { + Class clazz = classOrNull(cl, fqcn); + if (clazz == null) return null; + return project.getService(clazz); + } + + @Nullable + private static Object enumConst(@Nonnull ClassLoader cl, String fqcn, String name) { + return staticField(cl, fqcn, name); + } + + @Nullable + private static Object staticField(@Nonnull ClassLoader cl, String fqcn, String fieldName) { + try { + Class clazz = cl.loadClass(fqcn); + Field field = clazz.getField(fieldName); + return field.get(null); + } catch (Throwable t) { + return null; + } + } + + @Nullable + private static Object invoke0OrNull(Object target, String methodName) { + try { + for (Method m : target.getClass().getMethods()) { + if (m.getName().equals(methodName) && m.getParameterCount() == 0) { + m.setAccessible(true); + return m.invoke(target); + } + } + } catch (Throwable ignored) { + } + return null; + } +} 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 437c933a54..4d77aa148c 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 @@ -498,11 +498,20 @@ private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull Stri return false; } + // Prepare a fresh LOCAL session to avoid routing to non-Local providers (CLI/Cloud/Claude). + final String localSessionId = CopilotLocalSessionHelper.prepareLocalFallbackSession(project); + // Use Kotlin Function1 since the Copilot API is written in Kotlin Function1 queryBuilder = builder -> { try { builder.getClass().getMethod("withInput", String.class).invoke(builder, prompt); - builder.getClass().getMethod("withNewSession").invoke(builder); + // Use the pre-created LOCAL session when available (1.11+); otherwise withNewSession. + if (localSessionId != null) { + builder.getClass().getMethod("withExistingSession", String.class).invoke(builder, localSessionId); + } else { + builder.getClass().getMethod("withNewSession").invoke(builder); + } + //withAgentProviderLocal(builder, copilotClassLoader); withModelCompatibility(builder, DEFAULT_MODEL_NAME); Method withSessionIdReceiverMethod = findMethodByName(builder.getClass(), "withSessionIdReceiver"); if (withSessionIdReceiverMethod != null) { @@ -798,6 +807,45 @@ private void showGenericUpgradeGuidance(@Nonnull Project project, @Nonnull Strin Notifications.Bus.notify(guidanceNotification, project); } + /** + * Pins the query to the LOCAL agent provider via reflection. + * This ensures the Java Upgrade prompt runs on the local (CLS) agent which hosts the + * modernize-java-upgrade custom agent, instead of inheriting the home session's provider. + * Requires Copilot plugin version that includes QueryOptionBuilder.withAgentProvider(). + * Silently no-ops on older versions. + * + * @param builder the query option builder + * @param copilotClassLoader the Copilot plugin's classloader + */ + private static void withAgentProviderLocal(@Nonnull Object builder, @Nonnull ClassLoader copilotClassLoader) { + try { + final Class agentProviderClass = copilotClassLoader.loadClass("com.github.copilot.agent.agentProvider.AgentProvider"); + Object localProvider = null; + for (Object enumConstant : agentProviderClass.getEnumConstants()) { + if ("LOCAL".equals(((Enum) enumConstant).name())) { + localProvider = enumConstant; + break; + } + } + if (localProvider == null) { + log.info("withAgentProviderLocal: AgentProvider.LOCAL not found; skipping."); + return; + } + final Method withAgentProvider = findAccessibleMethod(builder.getClass(), "withAgentProvider", 1); + if (withAgentProvider != null) { + withAgentProvider.invoke(builder, localProvider); + log.info("withAgentProviderLocal: pinned query to AgentProvider.LOCAL"); + } else { + log.info("withAgentProviderLocal: withAgentProvider() not exposed by this Copilot version; skipping."); + } + } catch (ClassNotFoundException ex) { + // Older Copilot without AgentProvider enum; silently skip. + log.info("withAgentProviderLocal: AgentProvider class not found; skipping."); + } catch (Exception ex) { + log.warn("withAgentProviderLocal failed: " + ex.getMessage()); + } + } + /** * Sets the model for the query builder using reflection for compatibility with older versions of GitHub Copilot. * Note: The API 'withModel' is supported starting from Copilot version '1.5.63'. From 52b90681e3a7202fdaa892a6a9ddbd516efc5505 Mon Sep 17 00:00:00 2001 From: haozhang Date: Wed, 29 Jul 2026 14:46:01 +0800 Subject: [PATCH 2/2] with local agent provider via reflection --- .../service/CopilotLocalSessionHelper.java | 330 ------------------ .../JavaVersionNotificationService.java | 41 +-- 2 files changed, 12 insertions(+), 359 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java deleted file mode 100644 index 6c4688f648..0000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CopilotLocalSessionHelper.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; - -import com.intellij.ide.plugins.IdeaPluginDescriptor; -import com.intellij.ide.plugins.PluginManagerCore; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.extensions.PluginId; -import com.intellij.openapi.project.Project; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.UUID; - -/** - * Creates a fresh LOCAL chat session before sending a prompt via {@code CopilotChatService.query()}, - * ensuring prompts always route to the Local agent regardless of the user's selected provider. - * - *

On Copilot 1.11+, {@code query(withNewSession())} reuses the home session whose target follows - * the user's provider selection (BACKGROUND/CLOUD/CLAUDE). If the user has selected a non-Local - * provider, the prompt lands on the wrong agent. This helper dispatches - * {@code ChatAction.CreateSession(newId, PANEL, LOCAL, agentMode, null, true)} before the query, - * creating a race-free LOCAL session. The caller uses {@code withExistingSession(newId)} instead of - * {@code withNewSession()}. - * - *

On older Copilot (1.9/1.10) where {@code CreateSession} doesn't exist, this falls back to - * switching the provider to LOCAL via {@code TargetSelected} or the provider service, and returns - * null so the caller uses {@code withNewSession()}. - * - *

All Copilot types are loaded through the Copilot plugin's classloader (since this plugin - * is not in the same classloader hierarchy) and accessed reflectively so a single binary works - * across Copilot versions. - */ -public final class CopilotLocalSessionHelper { - - private static final Logger LOG = Logger.getInstance(CopilotLocalSessionHelper.class); - - private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; - - private static final String FQ_CHAT_SERVICE = "com.github.copilot.session.ChatService"; - private static final String FQ_CHAT_TYPE = "com.github.copilot.session.Chat$Type"; - private static final String FQ_CHAT_TARGET_TYPE = "com.github.copilot.session.ChatTarget$Type"; - private static final String FQ_CREATE_SESSION = "com.github.copilot.session.ChatAction$CreateSession"; - private static final String FQ_TARGET_SELECTED = "com.github.copilot.session.ChatAction$TargetSelected"; - private static final String FQ_USER_SELECTED_PROVIDER = "com.github.copilot.agent.agentProvider.UserSelectedAgentProviderService"; - private static final String FQ_AGENT_PROVIDER = "com.github.copilot.agent.agentProvider.AgentProvider"; - private static final String FQ_CHATMODE_BUILTINS = "com.github.copilot.agent.chatMode.ChatModeService$BuiltInChatModes"; - private static final String DEFAULT_CTOR_MARKER = "kotlin.jvm.internal.DefaultConstructorMarker"; - - private static final String TARGET_LOCAL = "LOCAL"; - - private CopilotLocalSessionHelper() {} - - /** - * Prepares a fresh LOCAL session for the caller's upcoming {@code CopilotChatService.query()}. - * - * @param project the current project - * @return a fresh LOCAL session ID for {@code withExistingSession(id)}, or {@code null} when - * the caller should fall back to {@code withNewSession()}. - */ - @Nullable - public static String prepareLocalFallbackSession(@Nonnull Project project) { - ClassLoader cl = getCopilotClassLoader(); - if (cl == null) { - LOG.debug("prepareLocalFallbackSession: Copilot classloader unavailable."); - return null; - } - try { - String sessionId = createFreshLocalSessionId(project, cl); - if (sessionId != null) { - return sessionId; - } - } catch (Throwable t) { - LOG.debug("Could not create a fresh LOCAL session; falling back to withNewSession.", t); - } - // 1.9/1.10 or CreateSession unavailable: try switching provider to LOCAL synchronously. - try { - ensureLocalProvider(project, cl); - } catch (Throwable t) { - LOG.debug("Failed to switch provider to Local; continuing.", t); - } - return null; - } - - @Nullable - private static ClassLoader getCopilotClassLoader() { - IdeaPluginDescriptor copilot = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); - if (copilot == null || !copilot.isEnabled()) return null; - return copilot.getPluginClassLoader(); - } - - /** - * 1.11+ only: dispatches {@code ChatAction.CreateSession(newId, PANEL, LOCAL, agentMode, null, true)} - * and returns newId. Returns null when CreateSession is absent (1.9/1.10). - */ - @Nullable - private static String createFreshLocalSessionId(@Nonnull Project project, @Nonnull ClassLoader cl) { - if (classOrNull(cl, FQ_CREATE_SESSION) == null) return null; - Object chatService = projectService(project, cl, FQ_CHAT_SERVICE); - if (chatService == null) return null; - - String sessionId = UUID.randomUUID().toString(); - Object panel = enumConst(cl, FQ_CHAT_TYPE, "PANEL"); - Object local = enumConst(cl, FQ_CHAT_TARGET_TYPE, TARGET_LOCAL); - String agentModeId = builtinAgentModeId(cl); - - Object createAction = constructPadded(cl, FQ_CREATE_SESSION, - new Object[]{sessionId, panel, local, agentModeId, null, true}); - dispatch(chatService, createAction); - LOG.info("Created fresh LOCAL session " + sessionId + " for appmod fallback send."); - return sessionId; - } - - /** - * Switches the provider to LOCAL. Tries ChatAction.TargetSelected (1.11+) first, then the - * UserSelectedAgentProviderService (1.9/1.10). - */ - private static void ensureLocalProvider(@Nonnull Project project, @Nonnull ClassLoader cl) { - if (trySwitchViaTargetSelected(project, cl)) { - LOG.info("Switched provider -> LOCAL via ChatAction.TargetSelected"); - return; - } - if (trySwitchViaProviderService(project, cl)) { - LOG.info("Switched provider -> LOCAL via UserSelectedAgentProviderService"); - } - } - - private static boolean trySwitchViaTargetSelected(@Nonnull Project project, @Nonnull ClassLoader cl) { - try { - Object chatService = projectService(project, cl, FQ_CHAT_SERVICE); - if (chatService == null) return false; - Object chatFlow = invoke0OrNull(chatService, "getChat"); - if (chatFlow == null) chatFlow = invoke0OrNull(chatService, "getPanelChat"); - if (chatFlow == null) return false; - Object chat = invoke0OrNull(chatFlow, "getValue"); - if (chat == null) return false; - Object activeId = invoke0OrNull(chat, "getActiveSessionId"); - if (!(activeId instanceof String) || ((String) activeId).isEmpty()) return false; - Object localTarget = enumConst(cl, FQ_CHAT_TARGET_TYPE, TARGET_LOCAL); - if (localTarget == null) return false; - Object action = constructByArity(cl, FQ_TARGET_SELECTED, 4, - new Object[]{activeId, localTarget, null, null}); - dispatch(chatService, action); - return true; - } catch (Throwable t) { - LOG.debug("Provider switch via ChatAction.TargetSelected failed.", t); - return false; - } - } - - private static boolean trySwitchViaProviderService(@Nonnull Project project, @Nonnull ClassLoader cl) { - try { - Object providerService = projectService(project, cl, FQ_USER_SELECTED_PROVIDER); - if (providerService == null) return false; - Object local = enumConst(cl, FQ_AGENT_PROVIDER, TARGET_LOCAL); - if (local == null) return false; - Method setter = null; - for (Method m : providerService.getClass().getMethods()) { - if ("setSelectedAgentProvider".equals(m.getName()) && m.getParameterCount() == 1) { - setter = m; - break; - } - } - if (setter == null) return false; - setter.setAccessible(true); - setter.invoke(providerService, local); - return true; - } catch (Throwable t) { - LOG.debug("Provider switch via UserSelectedAgentProviderService failed.", t); - return false; - } - } - - // ── Reflection utilities (all use Copilot's classloader) ───────────────── - - @Nullable - private static String builtinAgentModeId(@Nonnull ClassLoader cl) { - try { - Object builtins = staticField(cl, FQ_CHATMODE_BUILTINS, "INSTANCE"); - if (builtins == null) return null; - Object agentMode = invoke0OrNull(builtins, "getAgent"); - if (agentMode == null) return null; - Object id = invoke0OrNull(agentMode, "getId"); - return id instanceof String ? (String) id : null; - } catch (Throwable t) { - LOG.debug("Could not resolve builtin agent mode id.", t); - return null; - } - } - - private static Object constructPadded(@Nonnull ClassLoader cl, String fqcn, Object[] base) { - try { - Class clazz = cl.loadClass(fqcn); - Constructor ctor = canonicalConstructor(clazz); - if (ctor == null) throw new IllegalStateException("No usable constructor on " + fqcn); - ctor.setAccessible(true); - return ctor.newInstance(padArgs(ctor, base)); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Failed to construct " + fqcn, e); - } - } - - private static Object constructByArity(@Nonnull ClassLoader cl, String fqcn, int arity, Object[] args) { - try { - Class clazz = cl.loadClass(fqcn); - for (Constructor c : clazz.getDeclaredConstructors()) { - if (c.getParameterCount() == arity) { - Class[] paramTypes = c.getParameterTypes(); - if (paramTypes.length > 0 && DEFAULT_CTOR_MARKER.equals(paramTypes[paramTypes.length - 1].getName())) { - continue; - } - c.setAccessible(true); - return c.newInstance(args); - } - } - throw new IllegalStateException("No " + arity + "-arg constructor on " + fqcn); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Failed to construct " + fqcn, e); - } - } - - private static void dispatch(Object store, Object action) { - try { - Method method = null; - for (Method m : store.getClass().getMethods()) { - if ("dispatch".equals(m.getName()) && m.getParameterCount() == 1) { - method = m; - break; - } - } - if (method == null) throw new IllegalStateException("No dispatch(Action) on " + store.getClass().getName()); - method.setAccessible(true); - method.invoke(store, action); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("dispatch failed", e); - } - } - - @Nullable - private static Constructor canonicalConstructor(Class clazz) { - Constructor best = null; - for (Constructor c : clazz.getDeclaredConstructors()) { - Class[] paramTypes = c.getParameterTypes(); - if (paramTypes.length == 0) continue; - if (DEFAULT_CTOR_MARKER.equals(paramTypes[paramTypes.length - 1].getName())) continue; - if (best == null || c.getParameterCount() > best.getParameterCount()) { - best = c; - } - } - return best; - } - - private static Object[] padArgs(Constructor ctor, Object[] base) { - Class[] types = ctor.getParameterTypes(); - Object[] out = new Object[types.length]; - System.arraycopy(base, 0, out, 0, base.length); - for (int i = base.length; i < types.length; i++) { - out[i] = defaultForType(types[i]); - } - return out; - } - - private static Object defaultForType(Class type) { - if (!type.isPrimitive()) return null; - if (type == boolean.class) return false; - if (type == char.class) return '\0'; - if (type == long.class) return 0L; - if (type == float.class) return 0.0f; - if (type == double.class) return 0.0; - return 0; // int, byte, short - } - - @Nullable - private static Class classOrNull(@Nonnull ClassLoader cl, String fqcn) { - try { - return cl.loadClass(fqcn); - } catch (Throwable t) { - return null; - } - } - - @Nullable - private static Object projectService(@Nonnull Project project, @Nonnull ClassLoader cl, String fqcn) { - Class clazz = classOrNull(cl, fqcn); - if (clazz == null) return null; - return project.getService(clazz); - } - - @Nullable - private static Object enumConst(@Nonnull ClassLoader cl, String fqcn, String name) { - return staticField(cl, fqcn, name); - } - - @Nullable - private static Object staticField(@Nonnull ClassLoader cl, String fqcn, String fieldName) { - try { - Class clazz = cl.loadClass(fqcn); - Field field = clazz.getField(fieldName); - return field.get(null); - } catch (Throwable t) { - return null; - } - } - - @Nullable - private static Object invoke0OrNull(Object target, String methodName) { - try { - for (Method m : target.getClass().getMethods()) { - if (m.getName().equals(methodName) && m.getParameterCount() == 0) { - m.setAccessible(true); - return m.invoke(target); - } - } - } catch (Throwable ignored) { - } - return null; - } -} 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 4d77aa148c..2a6d1de74f 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 @@ -32,7 +32,6 @@ import java.util.HashSet; import java.util.Set; -import com.microsoft.azure.toolkit.lib.common.telemetry.AzureTelemeter; import kotlin.Unit; import kotlin.jvm.functions.Function1; import lombok.extern.slf4j.Slf4j; @@ -498,20 +497,12 @@ private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull Stri return false; } - // Prepare a fresh LOCAL session to avoid routing to non-Local providers (CLI/Cloud/Claude). - final String localSessionId = CopilotLocalSessionHelper.prepareLocalFallbackSession(project); - // Use Kotlin Function1 since the Copilot API is written in Kotlin Function1 queryBuilder = builder -> { try { builder.getClass().getMethod("withInput", String.class).invoke(builder, prompt); - // Use the pre-created LOCAL session when available (1.11+); otherwise withNewSession. - if (localSessionId != null) { - builder.getClass().getMethod("withExistingSession", String.class).invoke(builder, localSessionId); - } else { - builder.getClass().getMethod("withNewSession").invoke(builder); - } - //withAgentProviderLocal(builder, copilotClassLoader); + builder.getClass().getMethod("withNewSession").invoke(builder); + withLocalAgentProvider(builder, copilotClassLoader); withModelCompatibility(builder, DEFAULT_MODEL_NAME); Method withSessionIdReceiverMethod = findMethodByName(builder.getClass(), "withSessionIdReceiver"); if (withSessionIdReceiverMethod != null) { @@ -817,32 +808,24 @@ private void showGenericUpgradeGuidance(@Nonnull Project project, @Nonnull Strin * @param builder the query option builder * @param copilotClassLoader the Copilot plugin's classloader */ - private static void withAgentProviderLocal(@Nonnull Object builder, @Nonnull ClassLoader copilotClassLoader) { + private static void withLocalAgentProvider(@Nonnull Object builder, @Nonnull ClassLoader copilotClassLoader) { try { - final Class agentProviderClass = copilotClassLoader.loadClass("com.github.copilot.agent.agentProvider.AgentProvider"); - Object localProvider = null; - for (Object enumConstant : agentProviderClass.getEnumConstants()) { - if ("LOCAL".equals(((Enum) enumConstant).name())) { - localProvider = enumConstant; - break; - } - } - if (localProvider == null) { - log.info("withAgentProviderLocal: AgentProvider.LOCAL not found; skipping."); - return; - } + final Class typeClass = copilotClassLoader.loadClass("com.github.copilot.session.ChatTarget$Type"); + final Object localProvider = typeClass.getField("LOCAL").get(null); final Method withAgentProvider = findAccessibleMethod(builder.getClass(), "withAgentProvider", 1); if (withAgentProvider != null) { withAgentProvider.invoke(builder, localProvider); - log.info("withAgentProviderLocal: pinned query to AgentProvider.LOCAL"); + log.info("withLocalAgentProvider: pinned query to ChatTarget.Type.LOCAL"); } else { - log.info("withAgentProviderLocal: withAgentProvider() not exposed by this Copilot version; skipping."); + log.info("withLocalAgentProvider: withAgentProvider() not exposed by this Copilot version; skipping."); } } catch (ClassNotFoundException ex) { - // Older Copilot without AgentProvider enum; silently skip. - log.info("withAgentProviderLocal: AgentProvider class not found; skipping."); + // Older Copilot without ChatTarget.Type; silently skip. + log.info("withLocalAgentProvider: ChatTarget.Type class not found; skipping."); + } catch (NoSuchFieldException ex) { + log.info("withLocalAgentProvider: ChatTarget.Type.LOCAL field not found; skipping."); } catch (Exception ex) { - log.warn("withAgentProviderLocal failed: " + ex.getMessage()); + log.warn("withLocalAgentProvider failed: " + ex.getMessage()); } }