From 0209dacd8310d78b6897e39b8234996486e11903 Mon Sep 17 00:00:00 2001 From: xinyi-gong Date: Wed, 1 Jul 2026 13:10:49 +0800 Subject: [PATCH 1/4] feat: Auto-approve reads of Copilot customization files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading a Copilot customization file (skill, instruction, prompt, or agent) is normal agent operation, but these reads previously triggered a confirmation dialog — including workspace instructions caught by the default deny rule and user-global files outside the workspace. Recognize these reads precisely from the language server, which is the source of truth for where the files live (including user-configured and global locations). New copilot/customSkill|customPrompt|customInstruction| customAgent/list requests report each file's on-disk URI; a read is auto-approved when it exactly matches a reported file or sits inside a reported skill folder (covering SKILL.md and its helper files). Editing is never auto-approved — only reads. - Add CustomizationFileService, refreshed per type on the matching custom*/didChange notification, exposing an immutable path snapshot. - Hook FileOperationConfirmationHandler to auto-approve FILE_READ of a recognized customization file before the outside-workspace check. - Add FileUtils.isPathWithin for skill-folder containment. Tests: FileUtilsTests (isPathWithin), FileOperationConfirmationHandlerTests (isCustomizationRead), and TC-010/TC-011 in the auto-approve test plan. --- .../eclipse/core/utils/FileUtilsTests.java | 46 +++++++ .../service/CustomizationFileService.java | 118 ++++++++++++++++++ .../chat/service/IChatServiceManager.java | 5 + .../service/ICustomizationFileService.java | 46 +++++++ .../core/lsp/CopilotLanguageClient.java | 26 ++++ .../core/lsp/CopilotLanguageServer.java | 34 +++++ .../lsp/CopilotLanguageServerConnection.java | 42 +++++++ .../lsp/protocol/CustomizationFileInfo.java | 16 +++ .../lsp/protocol/WorkspaceFoldersParams.java | 22 ++++ .../copilot/eclipse/core/utils/FileUtils.java | 21 ++++ .../file-operation-auto-approve.md | 73 +++++++++++ ...FileOperationConfirmationHandlerTests.java | 90 +++++++++++++ .../FileOperationConfirmationHandler.java | 47 +++++++ .../ui/chat/services/ChatServiceManager.java | 9 ++ 14 files changed, 595 insertions(+) create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CustomizationFileInfo.java create mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java index cbbf6d55..ba557939 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java @@ -4,7 +4,9 @@ package com.microsoft.copilot.eclipse.core.utils; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; @@ -36,4 +38,48 @@ void testGetLocalFilePath_relativePath_returnsNull() { void testGetLocalFilePath_nonFileUri_returnsNull() { assertNull(FileUtils.getLocalFilePath("https://example.com/file.java")); } + + @Test + void testIsPathWithin_directChild_returnsTrue(@TempDir Path tempDir) { + assertTrue(FileUtils.isPathWithin(tempDir, tempDir.resolve("file.txt"), false)); + } + + @Test + void testIsPathWithin_nestedDescendant_returnsTrue(@TempDir Path tempDir) { + assertTrue(FileUtils.isPathWithin(tempDir, tempDir.resolve("sub/deep/file.txt"), false)); + } + + @Test + void testIsPathWithin_self_returnsTrueWhenNotStrict(@TempDir Path tempDir) { + assertTrue(FileUtils.isPathWithin(tempDir, tempDir, false)); + } + + @Test + void testIsPathWithin_self_returnsFalseWhenStrict(@TempDir Path tempDir) { + assertFalse(FileUtils.isPathWithin(tempDir, tempDir, true)); + } + + @Test + void testIsPathWithin_descendant_returnsTrueWhenStrict(@TempDir Path tempDir) { + assertTrue(FileUtils.isPathWithin(tempDir, tempDir.resolve("file.txt"), true)); + } + + @Test + void testIsPathWithin_sibling_returnsFalse(@TempDir Path tempDir) { + Path parent = tempDir.resolve("parent"); + Path sibling = tempDir.resolve("other/file.txt"); + assertFalse(FileUtils.isPathWithin(parent, sibling, false)); + } + + @Test + void testIsPathWithin_parent_returnsFalse(@TempDir Path tempDir) { + Path child = tempDir.resolve("child"); + assertFalse(FileUtils.isPathWithin(child, tempDir, false)); + } + + @Test + void testIsPathWithin_nullArguments_returnFalse(@TempDir Path tempDir) { + assertFalse(FileUtils.isPathWithin(null, tempDir, false)); + assertFalse(FileUtils.isPathWithin(tempDir, null, false)); + } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java new file mode 100644 index 00000000..2812f22f --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.chat.service; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.WorkspaceFolder; + +import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CustomizationFileInfo; +import com.microsoft.copilot.eclipse.core.utils.FileUtils; +import com.microsoft.copilot.eclipse.core.utils.WorkspaceUtils; + +/** + * Default {@link ICustomizationFileService} backed by the language server's {@code copilot/custom*} + * list requests. Each customization type is fetched and snapshotted independently. + */ +public class CustomizationFileService implements ICustomizationFileService { + + private final CopilotLanguageServerConnection lsConnection; + + private volatile Set promptFiles = Set.of(); + private volatile Set instructionFiles = Set.of(); + private volatile Set agentFiles = Set.of(); + private volatile Set skillFolders = Set.of(); + + private volatile Set customizationFiles = Set.of(); + + /** + * Creates the service. + * + * @param lsConnection the language server connection used to issue the list requests + */ + public CustomizationFileService(CopilotLanguageServerConnection lsConnection) { + this.lsConnection = lsConnection; + } + + @Override + public Set getCustomizationFiles() { + return customizationFiles; + } + + @Override + public Set getSkillFolders() { + return skillFolders; + } + + @Override + public void refresh() { + for (CustomizationType type : CustomizationType.values()) { + refresh(type); + } + } + + @Override + public void refresh(CustomizationType type) { + List workspaceFolders = WorkspaceUtils.listWorkspaceFolders(); + switch (type) { + case SKILL -> toPaths(lsConnection.listCustomSkills(workspaceFolders)).thenAccept(paths -> { + Set folders = new HashSet<>(); + for (Path skillFile : paths) { + Path parent = skillFile.getParent(); + if (parent != null) { + folders.add(parent); + } + } + this.skillFolders = Set.copyOf(folders); + }); + case PROMPT -> toPaths(lsConnection.listCustomPrompts(workspaceFolders)).thenAccept(paths -> { + this.promptFiles = Set.copyOf(paths); + rebuildCustomizationFiles(); + }); + case INSTRUCTION -> toPaths(lsConnection.listCustomInstructions(workspaceFolders)).thenAccept(paths -> { + this.instructionFiles = Set.copyOf(paths); + rebuildCustomizationFiles(); + }); + case AGENT -> toPaths(lsConnection.listCustomAgents(workspaceFolders)).thenAccept(paths -> { + this.agentFiles = Set.copyOf(paths); + rebuildCustomizationFiles(); + }); + default -> { + // No other customization types. + } + } + } + + private synchronized void rebuildCustomizationFiles() { + Set all = new HashSet<>(promptFiles); + all.addAll(instructionFiles); + all.addAll(agentFiles); + this.customizationFiles = Set.copyOf(all); + } + + private CompletableFuture> toPaths(CompletableFuture future) { + return future.thenApply(infos -> { + List paths = new ArrayList<>(); + if (infos != null) { + for (CustomizationFileInfo info : infos) { + Path path = FileUtils.getLocalFilePath(info.uri()); + if (path != null) { + paths.add(path); + } + } + } + return paths; + }).exceptionally(ex -> { + CopilotCore.LOGGER.error("Failed to list customization files", ex); + return List.of(); + }); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/IChatServiceManager.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/IChatServiceManager.java index 65aa911e..d6b0784f 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/IChatServiceManager.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/IChatServiceManager.java @@ -17,4 +17,9 @@ public interface IChatServiceManager { * Get the MCP config service. */ IMcpConfigService getMcpConfigService(); + + /** + * Get the customization file service tracking skill/prompt/instruction/agent file locations. + */ + ICustomizationFileService getCustomizationFileService(); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java new file mode 100644 index 00000000..c9cf2c7a --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.chat.service; + +import java.nio.file.Path; +import java.util.Set; + +/** + * Maintains an up-to-date view of the Copilot customization files (skills, prompts, instructions, + * and agents) reported by the language server, keyed by their on-disk location. + * + */ +public interface ICustomizationFileService { + + /** The customization kinds, each backed by a distinct language-server list request. */ + enum CustomizationType { + SKILL, PROMPT, INSTRUCTION, AGENT + } + + /** + * Returns the absolute paths of single-file customization files (prompts, instructions, agents). + * The returned set is an immutable snapshot. + */ + Set getCustomizationFiles(); + + /** + * Returns the absolute paths of skill folders (the directory containing each {@code SKILL.md}). + * A read of any file within one of these folders is a skill read. The returned set is an immutable + * snapshot. + */ + Set getSkillFolders(); + + /** + * Refreshes every customization type from the language server. Used for the initial load. + * Fire-and-forget; the work completes asynchronously off the language server's response. + */ + void refresh(); + + /** + * Refreshes only the given customization type, leaving the other snapshots untouched. Used to + * respond to a single {@code copilot/custom*/didChange} notification without re-fetching the + * unaffected types. Fire-and-forget; completes asynchronously off the language server's response. + */ + void refresh(CustomizationType type); +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java index da7aaee9..b702fca0 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java @@ -37,6 +37,7 @@ import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.FeatureFlags; import com.microsoft.copilot.eclipse.core.chat.service.IChatServiceManager; +import com.microsoft.copilot.eclipse.core.chat.service.ICustomizationFileService.CustomizationType; import com.microsoft.copilot.eclipse.core.chat.service.IMcpConfigService; import com.microsoft.copilot.eclipse.core.chat.service.IReferencedFileService; import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants; @@ -269,6 +270,7 @@ public void onRateLimitWarning(RateLimitWarningParams params) { @JsonNotification("copilot/customSkill/didChange") public void onDidChangeCustomSkill(Object params) { notifyCustomizationFilesChanged(); + refreshCustomizationFiles(CustomizationType.SKILL); } /** @@ -277,6 +279,23 @@ public void onDidChangeCustomSkill(Object params) { @JsonNotification("copilot/customPrompt/didChange") public void onDidChangeCustomPrompt(Object params) { notifyCustomizationFilesChanged(); + refreshCustomizationFiles(CustomizationType.PROMPT); + } + + /** + * Notify when custom instructions change (global or workspace). Signal-only; clients re-fetch the file list. + */ + @JsonNotification("copilot/customInstruction/didChange") + public void onDidChangeCustomInstruction(Object params) { + refreshCustomizationFiles(CustomizationType.INSTRUCTION); + } + + /** + * Notify when custom agents change (global or workspace). Signal-only; clients re-fetch the file list. + */ + @JsonNotification("copilot/customAgent/didChange") + public void onDidChangeCustomAgent(Object params) { + refreshCustomizationFiles(CustomizationType.AGENT); } private void notifyCustomizationFilesChanged() { @@ -285,6 +304,13 @@ private void notifyCustomizationFilesChanged() { } } + private void refreshCustomizationFiles(CustomizationType type) { + IChatServiceManager chatServiceManager = CopilotCore.getPlugin().getChatServiceManager(); + if (chatServiceManager != null && chatServiceManager.getCustomizationFileService() != null) { + chatServiceManager.getCustomizationFileService().refresh(type); + } + } + /** * Handles the Dynamic OAuth request for MCP. Shows a dialog with multiple input fields and returns the user's input * values. Returns null if the user cancels the request. diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java index 8f74d5dd..053a0d21 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java @@ -33,6 +33,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTurnParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CustomizationFileInfo; import com.microsoft.copilot.eclipse.core.lsp.protocol.DidShowInlineEditParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleResponse; @@ -51,6 +52,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.TelemetryExceptionParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.UpdateConversationToolsStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.UpdateMcpToolsStatusParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.WorkspaceFoldersParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokApiKey; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListApiKeyResponse; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListModelParams; @@ -148,6 +150,38 @@ public interface CopilotLanguageServer extends LanguageServer { @JsonRequest("conversation/templates") CompletableFuture listTemplates(ConversationTemplatesParams params); + /** + * List custom skill files (each carries its on-disk {@code uri}). + * + * @param params includes the workspace folders to scan + */ + @JsonRequest("copilot/customSkill/list") + CompletableFuture listCustomSkills(WorkspaceFoldersParams params); + + /** + * List custom prompt files (each carries its on-disk {@code uri}). + * + * @param params includes the workspace folders to scan + */ + @JsonRequest("copilot/customPrompt/list") + CompletableFuture listCustomPrompts(WorkspaceFoldersParams params); + + /** + * List custom instruction files (each carries its on-disk {@code uri}). + * + * @param params includes the workspace folders to scan + */ + @JsonRequest("copilot/customInstruction/list") + CompletableFuture listCustomInstructions(WorkspaceFoldersParams params); + + /** + * List custom agent files (each carries its on-disk {@code uri}). + * + * @param params includes the workspace folders to scan + */ + @JsonRequest("copilot/customAgent/list") + CompletableFuture listCustomAgents(WorkspaceFoldersParams params); + /** * List conversation modes. */ diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index de116abb..f841706b 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -50,6 +50,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTurnParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CustomizationFileInfo; import com.microsoft.copilot.eclipse.core.lsp.protocol.DidChangeCopilotWatchedFilesParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.DidShowInlineEditParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.GenerateThinkingTitleParams; @@ -72,6 +73,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.Turn; import com.microsoft.copilot.eclipse.core.lsp.protocol.UpdateConversationToolsStatusParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.UpdateMcpToolsStatusParams; +import com.microsoft.copilot.eclipse.core.lsp.protocol.WorkspaceFoldersParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokApiKey; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListApiKeyResponse; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListModelParams; @@ -379,6 +381,46 @@ public CompletableFuture listConversationTemplates(List< return this.languageServerWrapper.execute(fn); } + /** + * List custom skill files, each carrying its on-disk {@code uri}. + * + * @param workspaceFolders the workspace folders to scan + */ + public CompletableFuture listCustomSkills(List workspaceFolders) { + return this.languageServerWrapper.execute(server -> + ((CopilotLanguageServer) server).listCustomSkills(new WorkspaceFoldersParams(workspaceFolders))); + } + + /** + * List custom prompt files, each carrying its on-disk {@code uri}. + * + * @param workspaceFolders the workspace folders to scan + */ + public CompletableFuture listCustomPrompts(List workspaceFolders) { + return this.languageServerWrapper.execute(server -> + ((CopilotLanguageServer) server).listCustomPrompts(new WorkspaceFoldersParams(workspaceFolders))); + } + + /** + * List custom instruction files, each carrying its on-disk {@code uri}. + * + * @param workspaceFolders the workspace folders to scan + */ + public CompletableFuture listCustomInstructions(List workspaceFolders) { + return this.languageServerWrapper.execute(server -> + ((CopilotLanguageServer) server).listCustomInstructions(new WorkspaceFoldersParams(workspaceFolders))); + } + + /** + * List custom agent files, each carrying its on-disk {@code uri}. + * + * @param workspaceFolders the workspace folders to scan + */ + public CompletableFuture listCustomAgents(List workspaceFolders) { + return this.languageServerWrapper.execute(server -> + ((CopilotLanguageServer) server).listCustomAgents(new WorkspaceFoldersParams(workspaceFolders))); + } + /** * List the conversation modes. */ diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CustomizationFileInfo.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CustomizationFileInfo.java new file mode 100644 index 00000000..c164b1dd --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CustomizationFileInfo.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +/** + * Metadata for a Copilot customization file (skill, prompt, instruction, or agent) as returned by + * the language server's customization list requests. + * + * @param id the stable identifier, when provided by the language server + * @param name the user-facing name + * @param uri the on-disk file URI, or {@code null} for entries without a backing file + * @param storage the storage scope reported by the language server (for example {@code local} or {@code user}) + */ +public record CustomizationFileInfo(String id, String name, String uri, String storage) { +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java new file mode 100644 index 00000000..d955b366 --- /dev/null +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import java.util.Collections; +import java.util.List; + +import org.eclipse.lsp4j.WorkspaceFolder; + +/** + * Generic parameters for requests that scan a set of workspace folders, such as the + * {@code copilot/custom*} list requests. + * + * @param workspaceFolders the workspace folders to scan + */ +public record WorkspaceFoldersParams(List workspaceFolders) { + /** Compact constructor that defaults {@code null} workspace folders to an empty list. */ + public WorkspaceFoldersParams { + workspaceFolders = workspaceFolders != null ? workspaceFolders : Collections.emptyList(); + } +} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java index 06bbb611..4b71cacc 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java @@ -228,6 +228,27 @@ private static String stripFragment(String pathOrUri) { return fragmentIndex > 0 ? pathOrUri.substring(0, fragmentIndex) : pathOrUri; } + /** + * Returns whether {@code child} is the same path as {@code parent} or a descendant of it. Both + * paths are normalized to absolute form first, so this works for files that do not exist on disk. + * + * @param parent the candidate ancestor directory + * @param child the path to test for containment + * @param strict when {@code true}, {@code parent} equal to {@code child} is not a match + * @return {@code true} when {@code child} is contained within {@code parent} + */ + public static boolean isPathWithin(Path parent, Path child, boolean strict) { + if (parent == null || child == null) { + return false; + } + Path normalizedParent = parent.toAbsolutePath().normalize(); + Path normalizedChild = child.toAbsolutePath().normalize(); + if (strict && normalizedParent.equals(normalizedChild)) { + return false; + } + return normalizedChild.startsWith(normalizedParent); + } + /** * Normalizes a file path or URI string to a proper file URI string. Handles Windows absolute paths, POSIX absolute * paths, and existing URI strings. Line number fragments (e.g., #L123) are preserved. diff --git a/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-operation-auto-approve/file-operation-auto-approve.md b/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-operation-auto-approve/file-operation-auto-approve.md index 2a76f5f9..e5ddf4e8 100644 --- a/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-operation-auto-approve/file-operation-auto-approve.md +++ b/com.microsoft.copilot.eclipse.swtbot.test/test-plans/file-operation-auto-approve/file-operation-auto-approve.md @@ -340,3 +340,76 @@ approval #### 📸 Key Screenshots - [ ] Preference page after reset — only defaults. - [ ] `.java` file shows confirmation dialog post-reset. + +--- + +## 9. Customization file reads (skills, instructions, prompts, agents) + +Reading a Copilot customization file is always auto-approved, even with +"Auto approve file operations not covered by rules" **unchecked** and even +when a deny rule would otherwise match (e.g. the default `.github/instructions/*` +rule). The exemption is read-only and covers both workspace and user-global +(`~/.copilot`, `~/.claude`, `~/.agents`) locations. Recognized files: +`//**` (the whole skill folder, including helper files), +`*.instructions.md`, `*.prompt.md`, `*.agent.md`, and the fixed names +`copilot-instructions.md`, `git-commit-instructions.md`, `AGENTS.md`, `CLAUDE.md`. + +### TC-010: Customization reads auto-approve; edits and ordinary files still prompt + +**Type:** `Happy Path` +**Priority:** `P0` + +#### Preconditions +- "Auto approve file operations not covered by rules" is **unchecked**. +- The project contains `.github/skills/demo/SKILL.md` (whose body also tells + Copilot to read a sibling `notes.md`), `.github/instructions/coding.instructions.md`, + and `.github/copilot-instructions.md`. + +#### Steps +1. In Agent Mode, invoke the demo skill (or ask Copilot to read + `.github/skills/demo/SKILL.md`). +2. Observe **no confirmation** for `SKILL.md` **or** the referenced `notes.md`. +3. Type: `read the project instructions and summarize them`. +4. Observe **no confirmation** for `coding.instructions.md` — even though the + default `.github/instructions/*` deny rule matches. +5. Type: `read .github/copilot-instructions.md`. +6. Observe **no confirmation**. +7. Type: `add a comment line to .github/skills/demo/SKILL.md`. +8. Observe **confirmation dialog appears** — editing is not exempt. +9. Type: `read src/demo/App.java`. +10. Observe **confirmation dialog appears** — ordinary files are unaffected. + +#### Expected Result +- Reads of skill files (and their helpers), instruction/prompt/agent files, and + the fixed well-known files auto-approve with no dialog. +- Editing a customization file still prompts. +- Ordinary (non-customization) reads still prompt. + +#### 📸 Key Screenshots +- [ ] Skill `SKILL.md` + `notes.md` read with no confirmation. +- [ ] Instruction file read with no confirmation despite the deny rule. +- [ ] Edit to `SKILL.md` shows a confirmation dialog. + +--- + +### TC-011: User-global customization files auto-approve + +**Type:** `Edge Case` +**Priority:** `P1` + +#### Preconditions +- "Auto approve file operations not covered by rules" is **unchecked**. +- A user-global skill exists, e.g. `~/.copilot/skills/demo-global/SKILL.md`. + +#### Steps +1. In Agent Mode, invoke the global demo skill (or ask Copilot to read its + `SKILL.md`). +2. Observe **no confirmation** — global customization files are exempt even + though they live outside the workspace (an ordinary outside-workspace file + would still prompt, per TC-006). + +#### Expected Result +- User-global customization reads auto-approve without a dialog. + +#### 📸 Key Screenshots +- [ ] Global skill read with no confirmation. diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandlerTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandlerTests.java index 5d9b9573..ba60a62e 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandlerTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandlerTests.java @@ -9,15 +9,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import com.google.gson.Gson; import org.eclipse.jface.preference.IPreferenceStore; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -46,6 +49,9 @@ class FileOperationConfirmationHandlerTests { private AttachedFileRegistry attachedFileRegistry; private FileOperationConfirmationHandler handler; + @TempDir + private Path customizationBase; + @BeforeEach void setUp() { attachedFileRegistry = new AttachedFileRegistry(); @@ -677,6 +683,90 @@ void evaluate_priorityOrder_sessionFolderBeatsOutsideWorkspace() { assertTrue(evaluate(params, CONV_ID).isAutoApproved()); } + // --- customization file read recognition (isCustomizationRead) --- + + private Set customizationFiles() { + return Set.of( + customizationBase.resolve(".github/prompts/example.prompt.md"), + customizationBase.resolve(".github/instructions/coding.instructions.md"), + customizationBase.resolve(".github/agents/my.agent.md")); + } + + private Set skillFolders() { + return Set.of(customizationBase.resolve(".github/skills/demo")); + } + + @Test + void isCustomizationRead_promptFileExactMatch_isTrue() { + Path prompt = customizationBase.resolve(".github/prompts/example.prompt.md"); + assertTrue(FileOperationConfirmationHandler.isCustomizationRead( + prompt, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_instructionFileExactMatch_isTrue() { + Path instruction = customizationBase.resolve(".github/instructions/coding.instructions.md"); + assertTrue(FileOperationConfirmationHandler.isCustomizationRead( + instruction, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_agentFileExactMatch_isTrue() { + Path agent = customizationBase.resolve(".github/agents/my.agent.md"); + assertTrue(FileOperationConfirmationHandler.isCustomizationRead( + agent, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_skillMarkdownInsideFolder_isTrue() { + Path skill = customizationBase.resolve(".github/skills/demo/SKILL.md"); + assertTrue(FileOperationConfirmationHandler.isCustomizationRead( + skill, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_skillHelperFileInsideFolder_isTrue() { + Path helper = customizationBase.resolve(".github/skills/demo/notes.md"); + assertTrue(FileOperationConfirmationHandler.isCustomizationRead( + helper, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_nestedSkillHelperFile_isTrue() { + Path helper = customizationBase.resolve(".github/skills/demo/scripts/run.py"); + assertTrue(FileOperationConfirmationHandler.isCustomizationRead( + helper, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_unrelatedFile_isFalse() { + Path other = customizationBase.resolve("src/Main.java"); + assertFalse(FileOperationConfirmationHandler.isCustomizationRead( + other, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_promptOutsideReportedSet_isFalse() { + // A *.prompt.md the language server did not report must not be auto-approved. + Path loose = customizationBase.resolve("docs/other.prompt.md"); + assertFalse(FileOperationConfirmationHandler.isCustomizationRead( + loose, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_fileInUnreportedSkillFolder_isFalse() { + Path other = customizationBase.resolve(".github/skills/other/SKILL.md"); + assertFalse(FileOperationConfirmationHandler.isCustomizationRead( + other, customizationFiles(), skillFolders())); + } + + @Test + void isCustomizationRead_emptyInputs_isFalse() { + Path prompt = customizationBase.resolve(".github/prompts/example.prompt.md"); + assertFalse(FileOperationConfirmationHandler.isCustomizationRead( + prompt, Set.of(), Set.of())); + } + // --- Helpers --- private void stubRules(List rules) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java index 313c56c2..95b101f7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java @@ -28,8 +28,11 @@ import com.microsoft.copilot.eclipse.core.chat.ConfirmationContent; import com.microsoft.copilot.eclipse.core.chat.ConfirmationResult; import com.microsoft.copilot.eclipse.core.chat.FileOperationAutoApproveRule; +import com.microsoft.copilot.eclipse.core.chat.service.IChatServiceManager; +import com.microsoft.copilot.eclipse.core.chat.service.ICustomizationFileService; import com.microsoft.copilot.eclipse.core.lsp.protocol.InvokeClientToolConfirmationParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.ToolMetadata; +import com.microsoft.copilot.eclipse.core.utils.FileUtils; import com.microsoft.copilot.eclipse.ui.chat.Messages; /** @@ -171,6 +174,17 @@ private ConfirmationResult evaluateAutoApprovalEnabled( } } + // Auto-approve reads of Copilot customization files (skills, instructions, prompts, agents) + // discovered by the language server. Reading them is normal agent operation; edits still prompt. + if (isFileRead(params)) { + Path localPath = FileUtils.getLocalFilePath(filePath); + ICustomizationFileService service = getCustomizationFileService(); + if (localPath != null && service != null && isCustomizationRead( + localPath, service.getCustomizationFiles(), service.getSkillFolders())) { + return ConfirmationResult.AUTO_APPROVED; + } + } + // Files outside workspace always require confirmation if (isOutsideWorkspace(params)) { return ConfirmationResult.needsConfirmation(buildContent(params)); @@ -338,6 +352,39 @@ private boolean isOutsideWorkspace(InvokeClientToolConfirmationParams params) { return false; } + private static boolean isFileRead(InvokeClientToolConfirmationParams params) { + return FileToolType.fromValue(ConfirmationHandler.extractToolType(params)) + == FileToolType.FILE_READ; + } + + private static ICustomizationFileService getCustomizationFileService() { + IChatServiceManager chatServiceManager = CopilotCore.getPlugin().getChatServiceManager(); + return chatServiceManager == null ? null : chatServiceManager.getCustomizationFileService(); + } + + /** + * Returns whether reading {@code file} is a customization-file read that is safe to auto-approve. + * A read matches when the file exactly equals a language-server-reported customization file, or + * sits inside a reported skill folder (covering {@code SKILL.md} and the helper files it uses). + * + * @param file the absolute path being read + * @param customizationFiles reported single-file customizations (prompts, instructions, agents) + * @param skillFolders reported skill folders (each directory containing a {@code SKILL.md}) + * @return {@code true} when the read is a recognized customization read + */ + public static boolean isCustomizationRead(Path file, Set customizationFiles, Set skillFolders) { + Path normalized = file.toAbsolutePath().normalize(); + if (customizationFiles.contains(normalized)) { + return true; + } + for (Path skillFolder : skillFolders) { + if (FileUtils.isPathWithin(skillFolder, normalized, false)) { + return true; + } + } + return false; + } + /** Normalizes a file path for case-insensitive, separator-agnostic comparison. */ private static String normalizePath(String path) { return ConfirmationHandler.normalizePath(path); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java index 0883074f..17f15e94 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java @@ -5,6 +5,7 @@ import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.chat.service.CustomizationFileService; import com.microsoft.copilot.eclipse.core.chat.service.IChatServiceManager; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.persistence.ConversationPersistenceManager; @@ -29,6 +30,7 @@ public class ChatServiceManager implements IChatServiceManager { private ReferencedFileService referencedFileService; private McpConfigService mcpConfigService; private McpExtensionPointManager mcpExtensionPointManager; + private CustomizationFileService customizationFileService; private McpRuntimeLogger mcpRuntimeLogger; private ConversationPersistenceManager persistenceManager; @@ -51,6 +53,8 @@ public ChatServiceManager() { referencedFileService = new ReferencedFileService(); mcpConfigService = new McpConfigService(); mcpExtensionPointManager = new McpExtensionPointManager(mcpConfigService); + customizationFileService = new CustomizationFileService(this.lsConnection); + customizationFileService.refresh(); mcpRuntimeLogger = new McpRuntimeLogger(); persistenceManager = new ConversationPersistenceManager(this.authStatusManager); chatFontService = new ChatFontService(); @@ -140,6 +144,11 @@ public McpConfigService getMcpConfigService() { return mcpConfigService; } + @Override + public CustomizationFileService getCustomizationFileService() { + return customizationFileService; + } + /** * Get the persistence manager. * From 073d3af08596397e4c354fee71ba9919093c5298 Mon Sep 17 00:00:00 2001 From: xinyi-gong Date: Wed, 1 Jul 2026 13:33:48 +0800 Subject: [PATCH 2/4] rename refresh functions and merge workspaceFoldersParams --- .../service/CustomizationFileService.java | 10 ++++----- .../service/ICustomizationFileService.java | 10 +++++++-- .../core/lsp/CopilotLanguageClient.java | 6 +++++- .../core/lsp/CopilotLanguageServer.java | 3 +-- .../lsp/CopilotLanguageServerConnection.java | 3 +-- .../protocol/ConversationTemplatesParams.java | 21 ------------------- .../lsp/protocol/WorkspaceFoldersParams.java | 8 +++---- .../ui/chat/services/ChatServiceManager.java | 2 +- 8 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ConversationTemplatesParams.java diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java index 2812f22f..3b7a65b8 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java @@ -53,15 +53,15 @@ public Set getSkillFolders() { } @Override - public void refresh() { + public void refreshAll() { + List workspaceFolders = WorkspaceUtils.listWorkspaceFolders(); for (CustomizationType type : CustomizationType.values()) { - refresh(type); + refreshType(type, workspaceFolders); } } @Override - public void refresh(CustomizationType type) { - List workspaceFolders = WorkspaceUtils.listWorkspaceFolders(); + public void refreshType(CustomizationType type, List workspaceFolders) { switch (type) { case SKILL -> toPaths(lsConnection.listCustomSkills(workspaceFolders)).thenAccept(paths -> { Set folders = new HashSet<>(); @@ -105,7 +105,7 @@ private CompletableFuture> toPaths(CompletableFuture workspaceFolders); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java index b702fca0..f4ec5de9 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java @@ -70,6 +70,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.QuotaWarningParams; import com.microsoft.copilot.eclipse.core.utils.FileUtils; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.core.utils.WorkspaceUtils; /** * Language client for the Copilot language server. @@ -298,6 +299,8 @@ public void onDidChangeCustomAgent(Object params) { refreshCustomizationFiles(CustomizationType.AGENT); } + // Drives the slash-command service to re-fetch templates, which cover only skills and prompts; + // instruction and agent changes therefore do not need to post this. private void notifyCustomizationFilesChanged() { if (eventBroker != null) { eventBroker.post(CopilotEventConstants.TOPIC_CHAT_DID_CHANGE_CUSTOMIZATION_FILES, null); @@ -307,7 +310,8 @@ private void notifyCustomizationFilesChanged() { private void refreshCustomizationFiles(CustomizationType type) { IChatServiceManager chatServiceManager = CopilotCore.getPlugin().getChatServiceManager(); if (chatServiceManager != null && chatServiceManager.getCustomizationFileService() != null) { - chatServiceManager.getCustomizationFileService().refresh(type); + chatServiceManager.getCustomizationFileService() + .refreshType(type, WorkspaceUtils.listWorkspaceFolders()); } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java index 053a0d21..f238d933 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServer.java @@ -29,7 +29,6 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTemplate; -import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTemplatesParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTurnParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; @@ -148,7 +147,7 @@ public interface CopilotLanguageServer extends LanguageServer { * @param params includes workspace folders for discovering workspace-specific prompt files and skills */ @JsonRequest("conversation/templates") - CompletableFuture listTemplates(ConversationTemplatesParams params); + CompletableFuture listTemplates(WorkspaceFoldersParams params); /** * List custom skill files (each carries its on-disk {@code uri}). diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index f841706b..1c853da5 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -46,7 +46,6 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationMode; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationModesParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTemplate; -import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTemplatesParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationTurnParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; @@ -376,7 +375,7 @@ public CompletableFuture addConversationTurn(String workDoneToke */ public CompletableFuture listConversationTemplates(List workspaceFolders) { Function> fn = server -> { - return ((CopilotLanguageServer) server).listTemplates(new ConversationTemplatesParams(workspaceFolders)); + return ((CopilotLanguageServer) server).listTemplates(new WorkspaceFoldersParams(workspaceFolders)); }; return this.languageServerWrapper.execute(fn); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ConversationTemplatesParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ConversationTemplatesParams.java deleted file mode 100644 index 6b384b94..00000000 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ConversationTemplatesParams.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package com.microsoft.copilot.eclipse.core.lsp.protocol; - -import java.util.Collections; -import java.util.List; - -import org.eclipse.lsp4j.WorkspaceFolder; - -/** - * Parameters for the {@code conversation/templates} request. - * - * @param workspaceFolders the workspace folders used to discover workspace-specific prompt files and skills - */ -public record ConversationTemplatesParams(List workspaceFolders) { - /** Compact constructor that defaults {@code null} workspace folders to an empty list. */ - public ConversationTemplatesParams { - workspaceFolders = workspaceFolders != null ? workspaceFolders : Collections.emptyList(); - } -} diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java index d955b366..5f3e3574 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/WorkspaceFoldersParams.java @@ -3,20 +3,18 @@ package com.microsoft.copilot.eclipse.core.lsp.protocol; -import java.util.Collections; import java.util.List; import org.eclipse.lsp4j.WorkspaceFolder; /** - * Generic parameters for requests that scan a set of workspace folders, such as the - * {@code copilot/custom*} list requests. + * Generic parameters for language-server requests scoped to a set of workspace folders. * * @param workspaceFolders the workspace folders to scan */ public record WorkspaceFoldersParams(List workspaceFolders) { - /** Compact constructor that defaults {@code null} workspace folders to an empty list. */ + /** Compact constructor that defensively copies the folders, defaulting {@code null} to empty. */ public WorkspaceFoldersParams { - workspaceFolders = workspaceFolders != null ? workspaceFolders : Collections.emptyList(); + workspaceFolders = workspaceFolders != null ? List.copyOf(workspaceFolders) : List.of(); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java index 17f15e94..cda84adf 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java @@ -54,7 +54,7 @@ public ChatServiceManager() { mcpConfigService = new McpConfigService(); mcpExtensionPointManager = new McpExtensionPointManager(mcpConfigService); customizationFileService = new CustomizationFileService(this.lsConnection); - customizationFileService.refresh(); + customizationFileService.refreshAll(); mcpRuntimeLogger = new McpRuntimeLogger(); persistenceManager = new ConversationPersistenceManager(this.authStatusManager); chatFontService = new ChatFontService(); From 90c46a981354900efae1ffd91305099271679826 Mon Sep 17 00:00:00 2001 From: xinyi-gong Date: Wed, 1 Jul 2026 16:30:15 +0800 Subject: [PATCH 3/4] refactor: Decouple customization file service via event bus --- .../eclipse/core/utils/FileUtilsTests.java | 46 ------------------- .../service/CustomizationFileService.java | 40 +++++++++++++--- .../service/ICustomizationFileService.java | 15 +----- .../core/events/CopilotEventConstants.java | 4 +- .../core/lsp/CopilotLanguageClient.java | 35 +++++--------- .../copilot/eclipse/core/utils/FileUtils.java | 21 --------- .../FileOperationConfirmationHandler.java | 2 +- .../chat/services/ChatCompletionService.java | 9 +++- .../ui/chat/services/ChatServiceManager.java | 1 + 9 files changed, 58 insertions(+), 115 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java index ba557939..cbbf6d55 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/utils/FileUtilsTests.java @@ -4,9 +4,7 @@ package com.microsoft.copilot.eclipse.core.utils; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; @@ -38,48 +36,4 @@ void testGetLocalFilePath_relativePath_returnsNull() { void testGetLocalFilePath_nonFileUri_returnsNull() { assertNull(FileUtils.getLocalFilePath("https://example.com/file.java")); } - - @Test - void testIsPathWithin_directChild_returnsTrue(@TempDir Path tempDir) { - assertTrue(FileUtils.isPathWithin(tempDir, tempDir.resolve("file.txt"), false)); - } - - @Test - void testIsPathWithin_nestedDescendant_returnsTrue(@TempDir Path tempDir) { - assertTrue(FileUtils.isPathWithin(tempDir, tempDir.resolve("sub/deep/file.txt"), false)); - } - - @Test - void testIsPathWithin_self_returnsTrueWhenNotStrict(@TempDir Path tempDir) { - assertTrue(FileUtils.isPathWithin(tempDir, tempDir, false)); - } - - @Test - void testIsPathWithin_self_returnsFalseWhenStrict(@TempDir Path tempDir) { - assertFalse(FileUtils.isPathWithin(tempDir, tempDir, true)); - } - - @Test - void testIsPathWithin_descendant_returnsTrueWhenStrict(@TempDir Path tempDir) { - assertTrue(FileUtils.isPathWithin(tempDir, tempDir.resolve("file.txt"), true)); - } - - @Test - void testIsPathWithin_sibling_returnsFalse(@TempDir Path tempDir) { - Path parent = tempDir.resolve("parent"); - Path sibling = tempDir.resolve("other/file.txt"); - assertFalse(FileUtils.isPathWithin(parent, sibling, false)); - } - - @Test - void testIsPathWithin_parent_returnsFalse(@TempDir Path tempDir) { - Path child = tempDir.resolve("child"); - assertFalse(FileUtils.isPathWithin(child, tempDir, false)); - } - - @Test - void testIsPathWithin_nullArguments_returnFalse(@TempDir Path tempDir) { - assertFalse(FileUtils.isPathWithin(null, tempDir, false)); - assertFalse(FileUtils.isPathWithin(tempDir, null, false)); - } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java index 3b7a65b8..3c5a932c 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java @@ -10,9 +10,14 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; +import org.eclipse.e4.core.contexts.EclipseContextFactory; +import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.lsp4j.WorkspaceFolder; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.event.EventHandler; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; import com.microsoft.copilot.eclipse.core.lsp.protocol.CustomizationFileInfo; import com.microsoft.copilot.eclipse.core.utils.FileUtils; @@ -25,6 +30,8 @@ public class CustomizationFileService implements ICustomizationFileService { private final CopilotLanguageServerConnection lsConnection; + private final IEventBroker eventBroker; + private final EventHandler customizationFilesChangedHandler; private volatile Set promptFiles = Set.of(); private volatile Set instructionFiles = Set.of(); @@ -34,12 +41,30 @@ public class CustomizationFileService implements ICustomizationFileService { private volatile Set customizationFiles = Set.of(); /** - * Creates the service. + * Creates the service and subscribes to customization-file change events. * * @param lsConnection the language server connection used to issue the list requests */ public CustomizationFileService(CopilotLanguageServerConnection lsConnection) { this.lsConnection = lsConnection; + this.eventBroker = EclipseContextFactory + .getServiceContext(FrameworkUtil.getBundle(getClass()).getBundleContext()).get(IEventBroker.class); + this.customizationFilesChangedHandler = event -> { + if (event.getProperty(IEventBroker.DATA) instanceof CustomizationType type) { + CompletableFuture.runAsync(() -> refreshType(type, WorkspaceUtils.listWorkspaceFolders())); + } + }; + if (eventBroker != null) { + eventBroker.subscribe(CopilotEventConstants.TOPIC_CHAT_DID_CHANGE_CUSTOMIZATION_FILES, + customizationFilesChangedHandler); + } + } + + /** Unsubscribes from customization-file change events. */ + public void dispose() { + if (eventBroker != null) { + eventBroker.unsubscribe(customizationFilesChangedHandler); + } } @Override @@ -54,14 +79,15 @@ public Set getSkillFolders() { @Override public void refreshAll() { - List workspaceFolders = WorkspaceUtils.listWorkspaceFolders(); - for (CustomizationType type : CustomizationType.values()) { - refreshType(type, workspaceFolders); - } + CompletableFuture.runAsync(() -> { + List workspaceFolders = WorkspaceUtils.listWorkspaceFolders(); + for (CustomizationType type : CustomizationType.values()) { + refreshType(type, workspaceFolders); + } + }); } - @Override - public void refreshType(CustomizationType type, List workspaceFolders) { + private void refreshType(CustomizationType type, List workspaceFolders) { switch (type) { case SKILL -> toPaths(lsConnection.listCustomSkills(workspaceFolders)).thenAccept(paths -> { Set folders = new HashSet<>(); diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java index 5b8f7479..287b0432 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java @@ -4,11 +4,8 @@ package com.microsoft.copilot.eclipse.core.chat.service; import java.nio.file.Path; -import java.util.List; import java.util.Set; -import org.eclipse.lsp4j.WorkspaceFolder; - /** * Maintains an up-to-date view of the Copilot customization files (skills, prompts, instructions, * and agents) reported by the language server, keyed by their on-disk location. @@ -36,17 +33,7 @@ enum CustomizationType { /** * Refreshes every customization type from the language server. Used for the initial load. - * Fire-and-forget; the work completes asynchronously off the language server's response. + * Fire-and-forget; the work completes asynchronously off the caller's thread. */ void refreshAll(); - - /** - * Refreshes only the given customization type, leaving the other snapshots untouched. Used to - * respond to a single {@code copilot/custom*/didChange} notification without re-fetching the - * unaffected types. Fire-and-forget; completes asynchronously off the language server's response. - * - * @param type the customization type to refresh - * @param workspaceFolders the workspace folders to scan - */ - void refreshType(CustomizationType type, List workspaceFolders); } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java index 61637b76..1a2d3c9e 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/events/CopilotEventConstants.java @@ -183,8 +183,8 @@ public class CopilotEventConstants { public static final String TOPIC_QUOTA_WARNING = TOPIC_QUOTA + "WARNING"; /** - * Event when custom prompts, skills, agents, or instructions change on the language server. Clients should re-fetch - * conversation templates on receipt. + * Event when custom prompts, skills, agents, or instructions change on the language server. The event data is the + * {@link com.microsoft.copilot.eclipse.core.chat.service.ICustomizationFileService.CustomizationType} that changed. */ public static final String TOPIC_CHAT_DID_CHANGE_CUSTOMIZATION_FILES = TOPIC_CHAT + "DID_CHANGE_CUSTOMIZATION_FILES"; diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java index f4ec5de9..cf0e81ad 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java @@ -70,7 +70,6 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.QuotaWarningParams; import com.microsoft.copilot.eclipse.core.utils.FileUtils; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; -import com.microsoft.copilot.eclipse.core.utils.WorkspaceUtils; /** * Language client for the Copilot language server. @@ -266,52 +265,42 @@ public void onRateLimitWarning(RateLimitWarningParams params) { } /** - * Notify when custom skills change (global or workspace). Signal-only; clients re-fetch templates. + * Notify when custom skills change (global or workspace). */ @JsonNotification("copilot/customSkill/didChange") public void onDidChangeCustomSkill(Object params) { - notifyCustomizationFilesChanged(); - refreshCustomizationFiles(CustomizationType.SKILL); + postCustomizationFilesChanged(CustomizationType.SKILL); } /** - * Notify when custom prompts change (global or workspace). Signal-only; clients re-fetch templates. + * Notify when custom prompts change (global or workspace). */ @JsonNotification("copilot/customPrompt/didChange") public void onDidChangeCustomPrompt(Object params) { - notifyCustomizationFilesChanged(); - refreshCustomizationFiles(CustomizationType.PROMPT); + postCustomizationFilesChanged(CustomizationType.PROMPT); } /** - * Notify when custom instructions change (global or workspace). Signal-only; clients re-fetch the file list. + * Notify when custom instructions change (global or workspace). */ @JsonNotification("copilot/customInstruction/didChange") public void onDidChangeCustomInstruction(Object params) { - refreshCustomizationFiles(CustomizationType.INSTRUCTION); + postCustomizationFilesChanged(CustomizationType.INSTRUCTION); } /** - * Notify when custom agents change (global or workspace). Signal-only; clients re-fetch the file list. + * Notify when custom agents change (global or workspace). */ @JsonNotification("copilot/customAgent/didChange") public void onDidChangeCustomAgent(Object params) { - refreshCustomizationFiles(CustomizationType.AGENT); + postCustomizationFilesChanged(CustomizationType.AGENT); } - // Drives the slash-command service to re-fetch templates, which cover only skills and prompts; - // instruction and agent changes therefore do not need to post this. - private void notifyCustomizationFilesChanged() { + // Broadcasts the change on the event bus; subscribers (the customization-file service and the + // slash-command service) react without this class depending on them directly. + private void postCustomizationFilesChanged(CustomizationType type) { if (eventBroker != null) { - eventBroker.post(CopilotEventConstants.TOPIC_CHAT_DID_CHANGE_CUSTOMIZATION_FILES, null); - } - } - - private void refreshCustomizationFiles(CustomizationType type) { - IChatServiceManager chatServiceManager = CopilotCore.getPlugin().getChatServiceManager(); - if (chatServiceManager != null && chatServiceManager.getCustomizationFileService() != null) { - chatServiceManager.getCustomizationFileService() - .refreshType(type, WorkspaceUtils.listWorkspaceFolders()); + eventBroker.post(CopilotEventConstants.TOPIC_CHAT_DID_CHANGE_CUSTOMIZATION_FILES, type); } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java index 4b71cacc..06bbb611 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/utils/FileUtils.java @@ -228,27 +228,6 @@ private static String stripFragment(String pathOrUri) { return fragmentIndex > 0 ? pathOrUri.substring(0, fragmentIndex) : pathOrUri; } - /** - * Returns whether {@code child} is the same path as {@code parent} or a descendant of it. Both - * paths are normalized to absolute form first, so this works for files that do not exist on disk. - * - * @param parent the candidate ancestor directory - * @param child the path to test for containment - * @param strict when {@code true}, {@code parent} equal to {@code child} is not a match - * @return {@code true} when {@code child} is contained within {@code parent} - */ - public static boolean isPathWithin(Path parent, Path child, boolean strict) { - if (parent == null || child == null) { - return false; - } - Path normalizedParent = parent.toAbsolutePath().normalize(); - Path normalizedChild = child.toAbsolutePath().normalize(); - if (strict && normalizedParent.equals(normalizedChild)) { - return false; - } - return normalizedChild.startsWith(normalizedParent); - } - /** * Normalizes a file path or URI string to a proper file URI string. Handles Windows absolute paths, POSIX absolute * paths, and existing URI strings. Line number fragments (e.g., #L123) are preserved. diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java index 95b101f7..ba861ed4 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/confirmation/FileOperationConfirmationHandler.java @@ -378,7 +378,7 @@ public static boolean isCustomizationRead(Path file, Set customizationFile return true; } for (Path skillFolder : skillFolders) { - if (FileUtils.isPathWithin(skillFolder, normalized, false)) { + if (normalized.startsWith(skillFolder)) { return true; } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatCompletionService.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatCompletionService.java index cff99834..1f0953cd 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatCompletionService.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatCompletionService.java @@ -30,6 +30,7 @@ import com.microsoft.copilot.eclipse.core.CopilotAuthStatusListener; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.FeatureFlags; +import com.microsoft.copilot.eclipse.core.chat.service.ICustomizationFileService.CustomizationType; 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; @@ -76,7 +77,13 @@ public ChatCompletionService(CopilotLanguageServerConnection lsConnection, AuthS ResourcesPlugin.getWorkspace().addResourceChangeListener(skillFileListener, IResourceChangeEvent.POST_CHANGE); this.eventBroker = PlatformUI.getWorkbench().getService(IEventBroker.class); if (this.eventBroker != null) { - this.customPromptsChangedHandler = event -> fetchAsync(); + // Templates only surface skills and prompts, so ignore instruction/agent changes. + this.customPromptsChangedHandler = event -> { + Object type = event.getProperty(IEventBroker.DATA); + if (type == CustomizationType.SKILL || type == CustomizationType.PROMPT) { + fetchAsync(); + } + }; this.eventBroker.subscribe(CopilotEventConstants.TOPIC_CHAT_DID_CHANGE_CUSTOMIZATION_FILES, customPromptsChangedHandler); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java index cda84adf..18adad91 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java @@ -211,6 +211,7 @@ public void dispose() { this.agentToolService.dispose(); this.referencedFileService.dispose(); this.mcpConfigService.dispose(); + this.customizationFileService.dispose(); this.contextWindowService.dispose(); if (this.byokService != null) { this.byokService.dispose(); From 59702152ed6fd5f36c617e6cdce635f6cc7562f3 Mon Sep 17 00:00:00 2001 From: xinyi-gong Date: Thu, 2 Jul 2026 09:58:30 +0800 Subject: [PATCH 4/4] rename refreshAll to refreshAllAsync --- .../eclipse/core/chat/service/CustomizationFileService.java | 2 +- .../eclipse/core/chat/service/ICustomizationFileService.java | 2 +- .../copilot/eclipse/ui/chat/services/ChatServiceManager.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java index 3c5a932c..c94c14f7 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/CustomizationFileService.java @@ -78,7 +78,7 @@ public Set getSkillFolders() { } @Override - public void refreshAll() { + public void refreshAllAsync() { CompletableFuture.runAsync(() -> { List workspaceFolders = WorkspaceUtils.listWorkspaceFolders(); for (CustomizationType type : CustomizationType.values()) { diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java index 287b0432..736df418 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/chat/service/ICustomizationFileService.java @@ -35,5 +35,5 @@ enum CustomizationType { * Refreshes every customization type from the language server. Used for the initial load. * Fire-and-forget; the work completes asynchronously off the caller's thread. */ - void refreshAll(); + void refreshAllAsync(); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java index 18adad91..62e6c4a6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/ChatServiceManager.java @@ -54,7 +54,7 @@ public ChatServiceManager() { mcpConfigService = new McpConfigService(); mcpExtensionPointManager = new McpExtensionPointManager(mcpConfigService); customizationFileService = new CustomizationFileService(this.lsConnection); - customizationFileService.refreshAll(); + customizationFileService.refreshAllAsync(); mcpRuntimeLogger = new McpRuntimeLogger(); persistenceManager = new ConversationPersistenceManager(this.authStatusManager); chatFontService = new ChatFontService();