1010import java .util .Set ;
1111import java .util .concurrent .ExecutionException ;
1212
13+ import org .eclipse .core .resources .IResource ;
1314import org .eclipse .core .resources .IResourceChangeEvent ;
1415import org .eclipse .core .resources .IResourceChangeListener ;
1516import org .eclipse .core .resources .IResourceDelta ;
2021import org .eclipse .core .runtime .Status ;
2122import org .eclipse .core .runtime .jobs .Job ;
2223import org .eclipse .e4 .core .services .events .IEventBroker ;
23- import org .eclipse .jface .util .IPropertyChangeListener ;
2424import org .eclipse .lsp4j .WorkspaceFolder ;
2525import org .eclipse .ui .PlatformUI ;
2626import org .osgi .service .event .EventHandler ;
3737import com .microsoft .copilot .eclipse .core .lsp .protocol .ConversationTemplate ;
3838import com .microsoft .copilot .eclipse .core .lsp .protocol .CopilotScope ;
3939import com .microsoft .copilot .eclipse .core .lsp .protocol .CopilotStatusResult ;
40+ import com .microsoft .copilot .eclipse .core .lsp .protocol .TemplateSource ;
4041import com .microsoft .copilot .eclipse .core .utils .WorkspaceUtils ;
4142import com .microsoft .copilot .eclipse .ui .CopilotUi ;
4243
@@ -57,7 +58,6 @@ public class ChatCompletionService implements CopilotAuthStatusListener {
5758 private CopilotLanguageServerConnection lsConnection ;
5859 private AuthStatusManager authStatusManager ;
5960 private IResourceChangeListener skillFileListener ;
60- private IPropertyChangeListener preferenceListener ;
6161 private IEventBroker eventBroker ;
6262 private EventHandler customPromptsChangedHandler ;
6363
@@ -71,14 +71,10 @@ public ChatCompletionService(CopilotLanguageServerConnection lsConnection, AuthS
7171 this .authStatusManager = authStatusManager ;
7272 this .lsConnection = lsConnection ;
7373 this .authStatusManager .addCopilotAuthStatusListener (this );
74+ // TODO: Remove this listener once workspace-root is removed from workspaceFolders in CopilotLanguageClient as CLS
75+ // can watch the project prompt file change directly.
7476 this .skillFileListener = new SkillFileChangeListener ();
7577 ResourcesPlugin .getWorkspace ().addResourceChangeListener (skillFileListener , IResourceChangeEvent .POST_CHANGE );
76- this .preferenceListener = event -> {
77- if (Constants .ENABLE_SKILLS .equals (event .getProperty ())) {
78- fetchAsync ();
79- }
80- };
81- CopilotUi .getPlugin ().getLanguageServerSettingManager ().registerPropertyChangeListener (preferenceListener );
8278 this .eventBroker = PlatformUI .getWorkbench ().getService (IEventBroker .class );
8379 if (this .eventBroker != null ) {
8480 this .customPromptsChangedHandler = event -> fetchAsync ();
@@ -94,10 +90,10 @@ private void fetchAsync() {
9490 Job refreshJob = new Job ("Refresh slash commands service" ) {
9591 @ Override
9692 protected IStatus run (IProgressMonitor monitor ) {
93+ initConversationTemplates (monitor );
9794 if (monitor .isCanceled ()) {
9895 return Status .CANCEL_STATUS ;
9996 }
100- initConversationTemplates ();
10197 return Status .OK_STATUS ;
10298 }
10399
@@ -110,18 +106,25 @@ public boolean belongsTo(Object family) {
110106 refreshJob .schedule ();
111107 }
112108
113- private void initConversationTemplates () {
109+ private void initConversationTemplates (IProgressMonitor monitor ) {
114110 List <ConversationTemplate > newTemplates = new ArrayList <>();
115111 List <ConversationAgent > newAgents = new ArrayList <>();
116112 Set <String > newCommands = new HashSet <>();
113+ boolean skillsEnabled = isSkillsEnabled ();
117114
118115 // Command: /***
119116 // Pass workspace folders so the language server returns workspace-specific
120117 // prompt files (.prompt.md) and skills (SKILL.md) alongside built-in templates.
121118 try {
122119 List <WorkspaceFolder > workspaceFolders = WorkspaceUtils .listWorkspaceFolders ();
123120 ConversationTemplate [] rawTemplates = this .lsConnection .listConversationTemplates (workspaceFolders ).get ();
121+ if (monitor .isCanceled ()) {
122+ return ;
123+ }
124124 for (ConversationTemplate template : rawTemplates ) {
125+ if (!skillsEnabled && template .source () == TemplateSource .SKILL ) {
126+ continue ;
127+ }
125128 if (!EXCLUDED_COMMANDS .contains (template .id ())) {
126129 newTemplates .add (template );
127130 newCommands .add (TEMPLATE_MARK + template .id ());
@@ -131,9 +134,16 @@ private void initConversationTemplates() {
131134 CopilotCore .LOGGER .error (e );
132135 }
133136
137+ if (monitor .isCanceled ()) {
138+ return ;
139+ }
140+
134141 // Command: @***
135142 try {
136143 ConversationAgent [] rawAgents = this .lsConnection .listConversationAgents ().get ();
144+ if (monitor .isCanceled ()) {
145+ return ;
146+ }
137147 for (ConversationAgent agent : rawAgents ) {
138148 String agentSlug = agent .getSlug ();
139149 // @see ui.chat.ChatView#replaceWorkspaceCommand(String)
@@ -151,13 +161,24 @@ private void initConversationTemplates() {
151161 CopilotCore .LOGGER .error (e );
152162 }
153163
164+ if (monitor .isCanceled ()) {
165+ return ;
166+ }
167+
154168 // Atomically swap the cached data so readers always see a consistent snapshot.
155169 // Publish immutable snapshots so readers cannot accidentally mutate a live collection.
156170 this .templates = List .copyOf (newTemplates );
157171 this .agents = List .copyOf (newAgents );
158172 this .allCommands = Set .copyOf (newCommands );
159173 }
160174
175+ private boolean isSkillsEnabled () {
176+ CopilotCore plugin = CopilotCore .getPlugin ();
177+ FeatureFlags flags = plugin != null ? plugin .getFeatureFlags () : null ;
178+ return CopilotUi .getPlugin ().getPreferenceStore ().getBoolean (Constants .ENABLE_SKILLS )
179+ && flags != null && flags .isClientPreviewFeatureEnabled ();
180+ }
181+
161182 /**
162183 * Returns templates filtered by the scope appropriate for the given chat mode. In Agent mode only {@code agent-panel}
163184 * scoped templates (including skills) are shown; in Ask mode only {@code chat-panel} scoped templates are shown.
@@ -254,7 +275,6 @@ private void syncCommands(String status) {
254275 public void dispose () {
255276 this .authStatusManager .removeCopilotAuthStatusListener (this );
256277 ResourcesPlugin .getWorkspace ().removeResourceChangeListener (skillFileListener );
257- CopilotUi .getPlugin ().getLanguageServerSettingManager ().unregisterPropertyChangeListener (preferenceListener );
258278 if (this .eventBroker != null && this .customPromptsChangedHandler != null ) {
259279 this .eventBroker .unsubscribe (this .customPromptsChangedHandler );
260280 }
@@ -280,8 +300,10 @@ public void resourceChanged(IResourceChangeEvent event) {
280300 if (needsRefresh [0 ]) {
281301 return false ;
282302 }
283- String name = childDelta .getResource ().getName ();
284- if (SKILL_FILE_NAME .equals (name ) || name .endsWith (PROMPT_FILE_SUFFIX )) {
303+ if (!shouldVisitDelta (childDelta )) {
304+ return false ;
305+ }
306+ if (isPromptOrSkillFileDelta (childDelta )) {
285307 needsRefresh [0 ] = true ;
286308 return false ;
287309 }
@@ -294,5 +316,28 @@ public void resourceChanged(IResourceChangeEvent event) {
294316 fetchAsync ();
295317 }
296318 }
319+
320+ private boolean shouldVisitDelta (IResourceDelta delta ) {
321+ IResource resource = delta .getResource ();
322+ return resource != null && !resource .isDerived () && !resource .isTeamPrivateMember ();
323+ }
324+
325+ private boolean isPromptOrSkillFileDelta (IResourceDelta delta ) {
326+ IResource resource = delta .getResource ();
327+ if (resource .getType () != IResource .FILE || !isRelevantFileDelta (delta )) {
328+ return false ;
329+ }
330+
331+ String name = resource .getName ();
332+ return SKILL_FILE_NAME .equals (name ) || name .endsWith (PROMPT_FILE_SUFFIX );
333+ }
334+
335+ private boolean isRelevantFileDelta (IResourceDelta delta ) {
336+ int kind = delta .getKind ();
337+ if (kind == IResourceDelta .ADDED || kind == IResourceDelta .REMOVED ) {
338+ return true ;
339+ }
340+ return kind == IResourceDelta .CHANGED && (delta .getFlags () & IResourceDelta .CONTENT ) != 0 ;
341+ }
297342 }
298343}
0 commit comments