Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { onUnexpectedError } from '../../../../../base/common/errors.js';
7
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
8
import { autorun, derived, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';
9
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
10
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
11
import { IFileService } from '../../../../../platform/files/common/files.js';
12
import { ILabelService } from '../../../../../platform/label/common/label.js';
13
import { IProductService } from '../../../../../platform/product/common/productService.js';
14
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
15
import { IPathService } from '../../../../services/path/common/pathService.js';
16
import { IAICustomizationWorkspaceService, AICustomizationManagementSection } from '../../common/aiCustomizationWorkspaceService.js';
17
import { ICustomizationHarnessService, ICustomizationItemProvider, IHarnessDescriptor } from '../../common/customizationHarnessService.js';
18
import { IAgentPluginService } from '../../common/plugins/agentPluginService.js';
19
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
20
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
21
import { AICustomizationItemNormalizer, IAICustomizationItemSource, IAICustomizationListItem, ProviderCustomizationItemSource } from './aiCustomizationItemSource.js';
22
import { PromptsServiceCustomizationItemProvider } from './promptsServiceCustomizationItemProvider.js';
23
24
/**
25
* The set of sections whose items are sourced from the customization
26
* harness pipeline (extension-contributed providers, sync providers,
27
* and the prompts-service fallback). McpServers / Plugins / Models
28
* have their own dedicated services and are not modeled here.
29
*/
30
export const ITEMS_MODEL_SECTIONS = [
31
AICustomizationManagementSection.Agents,
32
AICustomizationManagementSection.Skills,
33
AICustomizationManagementSection.Instructions,
34
AICustomizationManagementSection.Prompts,
35
AICustomizationManagementSection.Hooks,
36
] as const;
37
38
export type ItemsModelSection = typeof ITEMS_MODEL_SECTIONS[number];
39
40
export const IAICustomizationItemsModel = createDecorator<IAICustomizationItemsModel>('aiCustomizationItemsModel');
41
42
/**
43
* Single source of truth for the items rendered by the AI Customizations
44
* editor and observed by sidebar surfaces (counts/badges).
45
*
46
* The model owns the per-active-harness `ProviderCustomizationItemSource`
47
* cache and exposes the unfiltered, normalized list of items per section.
48
* Both the editor and any sidebar surface read from these observables so
49
* there is exactly one discovery path for customizations.
50
*/
51
export interface IAICustomizationItemsModel {
52
readonly _serviceBrand: undefined;
53
54
/**
55
* Returns an observable of the unfiltered, normalized list items for the
56
* given prompts-based section under the currently active harness.
57
*/
58
getItems(section: ItemsModelSection): IObservable<readonly IAICustomizationListItem[]>;
59
60
/**
61
* Returns the live `ProviderCustomizationItemSource` for the active harness.
62
* Editor consumers may need this to access provider-level affordances
63
* (e.g. debug reporting). The returned source is reused across the
64
* lifetime of the active descriptor.
65
*/
66
getActiveItemSource(): IAICustomizationItemSource;
67
68
/**
69
* Convenience: an observable of the count for the given section.
70
*/
71
getCount(section: ItemsModelSection): IObservable<number>;
72
73
/**
74
* The fallback item provider used when the active descriptor has neither
75
* an `itemProvider` nor a `syncProvider`. Exposed for the debug report.
76
*/
77
getPromptsServiceItemProvider(): ICustomizationItemProvider;
78
79
/**
80
* Resolves once the most recent fetch for `section` has settled. Useful for
81
* tests / fixtures that need rendered output to reflect at least one fetch.
82
* Calling this also marks the section as observed (i.e. starts a fetch if
83
* none has been kicked off yet).
84
*/
85
whenSectionLoaded(section: ItemsModelSection): Promise<void>;
86
}
87
88
export class AICustomizationItemsModel extends Disposable implements IAICustomizationItemsModel {
89
declare readonly _serviceBrand: undefined;
90
91
private readonly itemNormalizer: AICustomizationItemNormalizer;
92
private readonly promptsServiceItemProvider: PromptsServiceCustomizationItemProvider;
93
94
/**
95
* Cached source per active descriptor. Keyed by descriptor reference (not id) so that
96
* an external harness re-registering under the same id (e.g. extension reload) gets a
97
* fresh source bound to the new provider. Pruned when its descriptor is no longer
98
* present in `availableHarnesses`.
99
*/
100
private readonly sourceCache = new Map<IHarnessDescriptor, IAICustomizationItemSource>();
101
102
private readonly perSection = new Map<ItemsModelSection, ISettableObservable<readonly IAICustomizationListItem[]>>();
103
private readonly perSectionCount = new Map<ItemsModelSection, IObservable<number>>();
104
private readonly fetchSeq = new Map<ItemsModelSection, number>();
105
/** Promise of the most recent fetch per section (resolves regardless of stale-discard). */
106
private readonly perSectionPending = new Map<ItemsModelSection, Promise<void>>();
107
/**
108
* Sections that have been observed at least once. The model only fetches on
109
* demand: first `getItems`/`getCount` for a section triggers an initial fetch,
110
* and subsequent harness/source/workspace change events refetch only sections
111
* that have already been read. This avoids 5x provider enumeration on startup.
112
*/
113
private readonly observedSections = new Set<ItemsModelSection>();
114
115
constructor(
116
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
117
@IPromptsService private readonly promptsService: IPromptsService,
118
@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,
119
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
120
@ILabelService labelService: ILabelService,
121
@IAgentPluginService agentPluginService: IAgentPluginService,
122
@IProductService productService: IProductService,
123
@IFileService private readonly fileService: IFileService,
124
@IPathService private readonly pathService: IPathService,
125
) {
126
super();
127
128
this.itemNormalizer = new AICustomizationItemNormalizer(
129
workspaceContextService,
130
workspaceService,
131
labelService,
132
agentPluginService,
133
productService,
134
);
135
this.promptsServiceItemProvider = new PromptsServiceCustomizationItemProvider(
136
() => this.harnessService.getActiveDescriptor(),
137
this.promptsService,
138
this.workspaceService,
139
productService,
140
);
141
142
for (const section of ITEMS_MODEL_SECTIONS) {
143
const items = observableValue<readonly IAICustomizationListItem[]>(`aiCustomizationItems:${section}`, []);
144
this.perSection.set(section, items);
145
this.perSectionCount.set(section, derived(reader => items.read(reader).length));
146
this.fetchSeq.set(section, 0);
147
}
148
149
// Re-bind to the active source whenever the active harness or the set of available
150
// harnesses changes (a new external provider may have registered for the already-
151
// active id), prune the source cache, and refetch any observed sections.
152
const sourceChangeListener = this._register(new MutableDisposable());
153
this._register(autorun(reader => {
154
const available = this.harnessService.availableHarnesses.read(reader);
155
this.harnessService.activeHarness.read(reader);
156
this.pruneSourceCache(available);
157
const descriptor = this.harnessService.getActiveDescriptor();
158
const source = this.getOrCreateSource(descriptor);
159
sourceChangeListener.value = source.onDidChange(() => this.refetchObserved(source));
160
this.refetchObserved(source);
161
}));
162
163
// Workspace folder changes / active project root changes affect the items the
164
// prompts service surfaces (e.g. workspace vs. user classification).
165
this._register(workspaceContextService.onDidChangeWorkspaceFolders(() => this.refetchObserved(this.getActiveItemSource())));
166
this._register(autorun(reader => {
167
this.workspaceService.activeProjectRoot.read(reader);
168
this.refetchObserved(this.getActiveItemSource());
169
}));
170
}
171
172
getItems(section: ItemsModelSection): IObservable<readonly IAICustomizationListItem[]> {
173
this.markObserved(section);
174
return this.perSection.get(section)!;
175
}
176
177
getCount(section: ItemsModelSection): IObservable<number> {
178
this.markObserved(section);
179
return this.perSectionCount.get(section)!;
180
}
181
182
getActiveItemSource(): IAICustomizationItemSource {
183
return this.getOrCreateSource(this.harnessService.getActiveDescriptor());
184
}
185
186
getPromptsServiceItemProvider(): ICustomizationItemProvider {
187
return this.promptsServiceItemProvider;
188
}
189
190
whenSectionLoaded(section: ItemsModelSection): Promise<void> {
191
this.markObserved(section);
192
return this.perSectionPending.get(section) ?? Promise.resolve();
193
}
194
195
private markObserved(section: ItemsModelSection): void {
196
if (this.observedSections.has(section) || this._store.isDisposed) {
197
return;
198
}
199
this.observedSections.add(section);
200
this.refetchSection(section, this.getActiveItemSource());
201
}
202
203
private getOrCreateSource(descriptor: IHarnessDescriptor): IAICustomizationItemSource {
204
const cached = this.sourceCache.get(descriptor);
205
if (cached) {
206
return cached;
207
}
208
const itemProvider = descriptor.itemProvider ?? (descriptor.syncProvider ? undefined : this.promptsServiceItemProvider);
209
const source = new ProviderCustomizationItemSource(
210
itemProvider,
211
descriptor.syncProvider,
212
this.promptsService,
213
this.workspaceService,
214
this.fileService,
215
this.pathService,
216
this.itemNormalizer,
217
);
218
this.sourceCache.set(descriptor, source);
219
return source;
220
}
221
222
private pruneSourceCache(available: readonly IHarnessDescriptor[]): void {
223
const live = new Set(available);
224
for (const descriptor of this.sourceCache.keys()) {
225
if (!live.has(descriptor)) {
226
this.sourceCache.delete(descriptor);
227
}
228
}
229
}
230
231
private refetchObserved(source: IAICustomizationItemSource): void {
232
for (const section of this.observedSections) {
233
this.refetchSection(section, source);
234
}
235
}
236
237
private refetchSection(section: ItemsModelSection, source: IAICustomizationItemSource): void {
238
const seq = (this.fetchSeq.get(section) ?? 0) + 1;
239
this.fetchSeq.set(section, seq);
240
const promptType = sectionToPromptType(section);
241
const observable = this.perSection.get(section)!;
242
const pending = source.fetchItems(promptType).then(items => {
243
if (this._store.isDisposed) {
244
return;
245
}
246
// Discard stale results (a newer fetch superseded this one).
247
if (this.fetchSeq.get(section) !== seq) {
248
return;
249
}
250
// Discard if the active source changed mid-fetch.
251
if (this.getActiveItemSource() !== source) {
252
return;
253
}
254
observable.set(items, undefined);
255
}, e => {
256
if (this._store.isDisposed) {
257
return;
258
}
259
onUnexpectedError(e);
260
});
261
this.perSectionPending.set(section, pending);
262
}
263
}
264
265
function sectionToPromptType(section: ItemsModelSection): PromptsType {
266
switch (section) {
267
case AICustomizationManagementSection.Agents: return PromptsType.agent;
268
case AICustomizationManagementSection.Skills: return PromptsType.skill;
269
case AICustomizationManagementSection.Instructions: return PromptsType.instructions;
270
case AICustomizationManagementSection.Hooks: return PromptsType.hook;
271
case AICustomizationManagementSection.Prompts:
272
default: return PromptsType.prompt;
273
}
274
}
275
276
registerSingleton(IAICustomizationItemsModel, AICustomizationItemsModel, InstantiationType.Delayed);
277
278