Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// 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.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;
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 final IEventBroker eventBroker;
private final EventHandler customizationFilesChangedHandler;

private volatile Set<Path> promptFiles = Set.of();
private volatile Set<Path> instructionFiles = Set.of();
private volatile Set<Path> agentFiles = Set.of();
private volatile Set<Path> skillFolders = Set.of();

private volatile Set<Path> customizationFiles = Set.of();

/**
* 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
public Set<Path> getCustomizationFiles() {
return customizationFiles;
}

@Override
public Set<Path> getSkillFolders() {
return skillFolders;
}

@Override
public void refreshAllAsync() {
CompletableFuture.runAsync(() -> {
List<WorkspaceFolder> workspaceFolders = WorkspaceUtils.listWorkspaceFolders();
for (CustomizationType type : CustomizationType.values()) {
refreshType(type, workspaceFolders);
}
});
}

private void refreshType(CustomizationType type, List<WorkspaceFolder> workspaceFolders) {
switch (type) {
Comment thread
xinyi-gong marked this conversation as resolved.
case SKILL -> toPaths(lsConnection.listCustomSkills(workspaceFolders)).thenAccept(paths -> {
Set<Path> 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<Path> all = new HashSet<>(promptFiles);
all.addAll(instructionFiles);
all.addAll(agentFiles);
this.customizationFiles = Set.copyOf(all);
}

private CompletableFuture<List<Path>> toPaths(CompletableFuture<CustomizationFileInfo[]> future) {
return future.thenApply(infos -> {
List<Path> paths = new ArrayList<>();
if (infos != null) {
for (CustomizationFileInfo info : infos) {
Path path = FileUtils.getLocalFilePath(info.uri());
if (path != null) {
paths.add(path.toAbsolutePath().normalize());
}
}
}
return paths;
}).exceptionally(ex -> {
CopilotCore.LOGGER.error("Failed to list customization files", ex);
return List.of();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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<Path> 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<Path> getSkillFolders();

/**
* 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 refreshAllAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -264,24 +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();
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();
postCustomizationFilesChanged(CustomizationType.PROMPT);
}

private void notifyCustomizationFilesChanged() {
/**
* Notify when custom instructions change (global or workspace).
*/
@JsonNotification("copilot/customInstruction/didChange")
public void onDidChangeCustomInstruction(Object params) {
postCustomizationFilesChanged(CustomizationType.INSTRUCTION);
}

/**
* Notify when custom agents change (global or workspace).
*/
@JsonNotification("copilot/customAgent/didChange")
public void onDidChangeCustomAgent(Object params) {
postCustomizationFilesChanged(CustomizationType.AGENT);
}
Comment thread
xinyi-gong marked this conversation as resolved.

// 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);
eventBroker.post(CopilotEventConstants.TOPIC_CHAT_DID_CHANGE_CUSTOMIZATION_FILES, type);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
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;
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;
Expand All @@ -51,6 +51,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;
Expand Down Expand Up @@ -146,7 +147,39 @@ public interface CopilotLanguageServer extends LanguageServer {
* @param params includes workspace folders for discovering workspace-specific prompt files and skills
*/
@JsonRequest("conversation/templates")
CompletableFuture<ConversationTemplate[]> listTemplates(ConversationTemplatesParams params);
CompletableFuture<ConversationTemplate[]> listTemplates(WorkspaceFoldersParams 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<CustomizationFileInfo[]> 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<CustomizationFileInfo[]> 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<CustomizationFileInfo[]> 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<CustomizationFileInfo[]> listCustomAgents(WorkspaceFoldersParams params);

/**
* List conversation modes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@
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;
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;
Expand All @@ -72,6 +72,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;
Expand Down Expand Up @@ -374,11 +375,51 @@ public CompletableFuture<ChatTurnResult> addConversationTurn(String workDoneToke
*/
public CompletableFuture<ConversationTemplate[]> listConversationTemplates(List<WorkspaceFolder> workspaceFolders) {
Function<LanguageServer, CompletableFuture<ConversationTemplate[]>> fn = server -> {
return ((CopilotLanguageServer) server).listTemplates(new ConversationTemplatesParams(workspaceFolders));
return ((CopilotLanguageServer) server).listTemplates(new WorkspaceFoldersParams(workspaceFolders));
};
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<CustomizationFileInfo[]> listCustomSkills(List<WorkspaceFolder> 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<CustomizationFileInfo[]> listCustomPrompts(List<WorkspaceFolder> 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<CustomizationFileInfo[]> listCustomInstructions(List<WorkspaceFolder> 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<CustomizationFileInfo[]> listCustomAgents(List<WorkspaceFolder> workspaceFolders) {
return this.languageServerWrapper.execute(server ->
((CopilotLanguageServer) server).listCustomAgents(new WorkspaceFoldersParams(workspaceFolders)));
}

/**
* List the conversation modes.
*/
Expand Down
Loading
Loading