From f50caa19a84e9babe54b8676247ed63128d58328 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 04:27:07 +0000 Subject: [PATCH 1/2] Initial plan From 067f3284dcbf059f6530975003450a4b865ff0f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 04:30:52 +0000 Subject: [PATCH 2/2] Consolidate built-in chat mode loading into manager Co-authored-by: jdneo <6193897+jdneo@users.noreply.github.com> --- .../chat/BuiltInChatModeManagerTests.java | 113 ++++++++++++++++++ .../service/BuiltInChatModeServiceTests.java | 75 ------------ .../core/chat/BuiltInChatModeManager.java | 57 +++++++++ .../chat/service/BuiltInChatModeService.java | 81 ------------- .../chat/services/UserPreferenceService.java | 3 +- 5 files changed, 171 insertions(+), 158 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManagerTests.java delete mode 100644 com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeServiceTests.java delete mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeService.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManagerTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManagerTests.java new file mode 100644 index 000000000..f62036ac4 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManagerTests.java @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.chat; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode; +import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams; + +@ExtendWith(MockitoExtension.class) +class BuiltInChatModeManagerTests { + + @Mock + private CopilotLanguageServerConnection mockConnection; + + @Test + void testLoadBuiltInModes_inlineAgentSkippedFromAgentModes() { + ConversationMode agentMode = createBuiltInMode("Agent", "Agent", "Agent", + "Advanced agent mode with access to tools and capabilities"); + ConversationMode inlineAgentMode = createBuiltInMode("InlineAgent", "Agent", "InlineAgent", + "Agent mode with a restricted tool set for inline editing"); + + when(mockConnection.listConversationModes(any(ConversationModesParams.class))) + .thenReturn(CompletableFuture.completedFuture(new ConversationMode[] { agentMode, inlineAgentMode })); + + List builtInModes = BuiltInChatModeManager.INSTANCE.loadBuiltInModes(mockConnection).join(); + + assertEquals(1, builtInModes.size()); + + BuiltInChatMode builtInMode = builtInModes.get(0); + assertNotNull(builtInMode); + assertEquals("Agent", builtInMode.getId()); + assertEquals("Agent", builtInMode.getDisplayName()); + assertEquals("Agent", builtInMode.getKind()); + } + + @Test + void testLoadBuiltInModes_onlyAllowedBuiltInNames() { + ConversationMode ask = createBuiltInMode("ask", "aSk", "Ask", "Ask mode"); + ConversationMode debugger = createBuiltInMode("debugger", "Debugger", "Debugger", "Debugger mode"); + ConversationMode custom = createBuiltInMode("custom", "Plan", "Plan", "Custom mode"); + custom.setBuiltIn(false); + ConversationMode unknown = createBuiltInMode("unknown", "Unknown", "Unknown", "Unknown mode"); + + when(mockConnection.listConversationModes(any(ConversationModesParams.class))) + .thenReturn(CompletableFuture.completedFuture(new ConversationMode[] { + null, custom, unknown, ask, debugger })); + + List modes = BuiltInChatModeManager.INSTANCE.loadBuiltInModes(mockConnection).join(); + + assertEquals(List.of("ask", "debugger"), modes.stream().map(BuiltInChatMode::getId).toList()); + } + + @Test + void testLoadBuiltInModes_nullConnectionReturnsEmptyList() { + assertEquals(List.of(), BuiltInChatModeManager.INSTANCE.loadBuiltInModes(null).join()); + } + + @Test + void testLoadBuiltInModes_conversionFailureSkipsMode() { + ConversationMode invalid = mock(ConversationMode.class); + when(invalid.isBuiltIn()).thenReturn(true); + when(invalid.getName()).thenReturn("Ask"); + when(invalid.getId()).thenReturn("invalid"); + when(invalid.getCustomTools()).thenThrow(new IllegalArgumentException("invalid tools")); + ConversationMode agent = createBuiltInMode("agent", "Agent", "Agent", "Agent mode"); + when(mockConnection.listConversationModes(any(ConversationModesParams.class))) + .thenReturn(CompletableFuture.completedFuture(new ConversationMode[] { invalid, agent })); + + List modes = BuiltInChatModeManager.INSTANCE.loadBuiltInModes(mockConnection).join(); + + assertEquals(List.of("agent"), modes.stream().map(BuiltInChatMode::getId).toList()); + } + + @Test + void testLoadBuiltInModes_rpcFailurePropagates() { + IllegalStateException failure = new IllegalStateException("RPC failed"); + when(mockConnection.listConversationModes(any(ConversationModesParams.class))) + .thenReturn(CompletableFuture.failedFuture(failure)); + + CompletionException exception = assertThrows(CompletionException.class, + () -> BuiltInChatModeManager.INSTANCE.loadBuiltInModes(mockConnection).join()); + + assertSame(failure, exception.getCause()); + } + + private ConversationMode createBuiltInMode(String id, String name, String kind, String description) { + ConversationMode mode = new ConversationMode(); + mode.setId(id); + mode.setName(name); + mode.setKind(kind); + mode.setBuiltIn(true); + mode.setDescription(description); + return mode; + } +} diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeServiceTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeServiceTests.java deleted file mode 100644 index 22039b462..000000000 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeServiceTests.java +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package com.microsoft.copilot.eclipse.core.chat.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Field; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.chat.BuiltInChatMode; -import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode; -import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams; - -@ExtendWith(MockitoExtension.class) -class BuiltInChatModeServiceTests { - - @Mock - private CopilotLanguageServerConnection mockConnection; - - private BuiltInChatModeService builtInChatModeService; - - @BeforeEach - void setUp() throws Exception { - builtInChatModeService = new BuiltInChatModeService(); - - CopilotCore plugin = new CopilotCore(); - Field languageServerField = CopilotCore.class.getDeclaredField("copilotLanguageServer"); - languageServerField.setAccessible(true); - languageServerField.set(plugin, mockConnection); - } - - @Test - void testLoadBuiltInModes_inlineAgentSkippedFromAgentModes() { - ConversationMode agentMode = createBuiltInMode("Agent", "Agent", "Agent", - "Advanced agent mode with access to tools and capabilities"); - ConversationMode inlineAgentMode = createBuiltInMode("InlineAgent", "Agent", "InlineAgent", - "Agent mode with a restricted tool set for inline editing"); - - when(mockConnection.listConversationModes(any(ConversationModesParams.class))) - .thenReturn(CompletableFuture.completedFuture(new ConversationMode[] { agentMode, inlineAgentMode })); - - List builtInModes = builtInChatModeService.loadBuiltInModes().join(); - - assertEquals(1, builtInModes.size()); - - BuiltInChatMode builtInMode = builtInModes.get(0); - assertNotNull(builtInMode); - assertEquals("Agent", builtInMode.getId()); - assertEquals("Agent", builtInMode.getDisplayName()); - assertEquals("Agent", builtInMode.getKind()); - } - - private ConversationMode createBuiltInMode(String id, String name, String kind, String description) { - ConversationMode mode = new ConversationMode(); - mode.setId(id); - mode.setName(name); - mode.setKind(kind); - mode.setBuiltIn(true); - mode.setDescription(description); - return mode; - } -} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManager.java index fd4199bbd..1f61a98b9 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/BuiltInChatModeManager.java @@ -4,7 +4,15 @@ package com.microsoft.copilot.eclipse.core.chat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode; +import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams; /** * Shared snapshot of built-in chat modes discovered asynchronously by the chat lifecycle. @@ -12,8 +20,57 @@ public enum BuiltInChatModeManager { INSTANCE; + private static final List ALLOWED_BUILTIN_NAMES = Arrays.asList(BuiltInChatMode.ASK_MODE_NAME, + BuiltInChatMode.AGENT_MODE_NAME, BuiltInChatMode.PLAN_MODE_NAME, BuiltInChatMode.DEBUGGER_MODE_NAME); + private volatile List builtInModes = List.of(); + /** + * Loads built-in modes using the owning chat lifecycle's language-server connection. The LSP requires workspace + * folders even though built-in modes do not depend on workspace context. + * + * @param lsConnection the connection used by the chat services + * @return the discovered built-in modes + */ + public CompletableFuture> loadBuiltInModes(CopilotLanguageServerConnection lsConnection) { + if (lsConnection == null) { + return CompletableFuture.completedFuture(new ArrayList<>()); + } + ConversationModesParams params = new ConversationModesParams(Collections.emptyList()); + + return lsConnection.listConversationModes(params).thenApply(conversationModes -> { + List modes = new ArrayList<>(); + + for (ConversationMode mode : conversationModes) { + if (mode == null || !mode.isBuiltIn()) { + continue; + } + // Exclude InlineAgent kind — it is not a user-facing chat mode + if (BuiltInChatMode.INLINE_AGENT_KIND.equalsIgnoreCase(mode.getKind())) { + continue; + } + // Filter to only allowed built-in modes by name (case-insensitive) + if (ALLOWED_BUILTIN_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(mode.getName()))) { + BuiltInChatMode builtIn = convertToBuiltInChatMode(mode); + if (builtIn != null) { + modes.add(builtIn); + } + } + } + + return modes; + }); + } + + private BuiltInChatMode convertToBuiltInChatMode(ConversationMode mode) { + try { + return new BuiltInChatMode(mode); + } catch (Exception e) { + CopilotCore.LOGGER.error("Failed to convert built-in mode: " + mode.getId(), e); + return null; + } + } + public List getBuiltInModes() { return new ArrayList<>(builtInModes); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeService.java deleted file mode 100644 index e005f198c..000000000 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/BuiltInChatModeService.java +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package com.microsoft.copilot.eclipse.core.chat.service; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.core.chat.BuiltInChatMode; -import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; -import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode; -import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams; - -/** - * Service for loading built-in chat modes from the LSP API. Built-in modes include Ask, Agent, and Plan. - */ -public class BuiltInChatModeService { - - private static final List ALLOWED_BUILTIN_NAMES = Arrays.asList(BuiltInChatMode.ASK_MODE_NAME, - BuiltInChatMode.AGENT_MODE_NAME, BuiltInChatMode.PLAN_MODE_NAME, BuiltInChatMode.DEBUGGER_MODE_NAME); - - /** - * Loads built-in modes from the LSP API. Only modes with names in ALLOWED_BUILTIN_NAMES are returned. - * - *

Note: The LSP requires workspace folders to be passed even for loading built-in modes. While built-in modes - * don't depend on workspace context, the LSP API enforces this parameter. - */ - public CompletableFuture> loadBuiltInModes() { - return loadBuiltInModes(CopilotCore.getPlugin().getCopilotLanguageServer()); - } - - /** - * Loads built-in modes using the owning chat lifecycle's language-server connection. - * - * @param lspConnection the connection used by the chat services - * @return the discovered built-in modes - */ - public CompletableFuture> loadBuiltInModes( - CopilotLanguageServerConnection lspConnection) { - if (lspConnection == null) { - return CompletableFuture.completedFuture(new ArrayList<>()); - } - ConversationModesParams params = new ConversationModesParams(Collections.emptyList()); - - return lspConnection.listConversationModes(params).thenApply(conversationModes -> { - List builtInModes = new ArrayList<>(); - - for (ConversationMode mode : conversationModes) { - if (mode == null || !mode.isBuiltIn()) { - continue; - } - // Exclude InlineAgent kind — it is not a user-facing chat mode - if (BuiltInChatMode.INLINE_AGENT_KIND.equalsIgnoreCase(mode.getKind())) { - continue; - } - // Filter to only allowed built-in modes by name (case-insensitive) - if (ALLOWED_BUILTIN_NAMES.stream().anyMatch(name -> name.equalsIgnoreCase(mode.getName()))) { - BuiltInChatMode builtIn = convertToBuiltInChatMode(mode); - if (builtIn != null) { - builtInModes.add(builtIn); - } - } - } - - return builtInModes; - }); - } - - private BuiltInChatMode convertToBuiltInChatMode(ConversationMode mode) { - try { - return new BuiltInChatMode(mode); - } catch (Exception e) { - CopilotCore.LOGGER.error("Failed to convert built-in mode: " + mode.getId(), e); - return null; - } - } -} \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/UserPreferenceService.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/UserPreferenceService.java index d7e9f1397..16ec1618b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/UserPreferenceService.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/UserPreferenceService.java @@ -30,7 +30,6 @@ import com.microsoft.copilot.eclipse.core.chat.CustomChatModeManager; import com.microsoft.copilot.eclipse.core.chat.InputNavigation; import com.microsoft.copilot.eclipse.core.chat.UserPreference; -import com.microsoft.copilot.eclipse.core.chat.service.BuiltInChatModeService; import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatMode; @@ -145,7 +144,7 @@ private void reloadBuiltInModes() { String account = authStatusManager.getUserName(); modeDiscoveryState.setValue(ModeDiscoveryState.LOADING); CompletableFuture> discovery = CompletableFuture - .supplyAsync(() -> new BuiltInChatModeService().loadBuiltInModes(lsConnection)) + .supplyAsync(() -> BuiltInChatModeManager.INSTANCE.loadBuiltInModes(lsConnection)) .thenCompose(result -> result); modeDiscovery = discovery; discovery.thenAccept(modes -> ensureRealm(() -> {