agentPluginServiceImpl.ts ×26

Frontier kind: Code frontier

unlabeled · c_48765756b70a

45 tests · 43998 LOC · 206 files · introduces 0 tests · 174 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
26 ranges174 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
4178 ranges43998 lines · 206 files · Browse complete extent
All tests (intent)
45 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 174 introduced LOC across 26 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/plugins/agentPluginServiceImpl.ts 174 introduced LOC · 26 ranges

Open complete file

84 * falling back to the first workspace folder for plugins outside the workspace.
85 */
86 > function resolveWorkspaceRoot(pluginUri: URI, workspaceContextService: IWorkspaceContextService): URI | undefined { agentPluginServiceImpl.ts
87 > const defaultFolder = workspaceContextService.getWorkspace().folders[0];
88 > const folder = workspaceContextService.getWorkspaceFolder(pluginUri) ?? defaultFolder;
89 > return folder?.uri;
90 > }
91
92 export class AgentPluginService extends Disposable implements IAgentPluginService {
271
272 protected async _refreshPlugins(): Promise<void> {
273 > const version = ++this._discoverVersion; agentPluginServiceImpl.ts
274 > const plugins = await this._discoverAndBuildPlugins(version);
275 > if (!this._isCurrentRefresh(version)) {
276 return;
277 }
279 > this._plugins.set(plugins, undefined);
280 > }
281
282 /** Subclasses return plugin sources to discover. */
284
285 private async _discoverAndBuildPlugins(version: number): Promise<readonly IAgentPlugin[]> {
286 > const sources = await this._discoverPluginSources(); agentPluginServiceImpl.ts
287 > if (!this._isCurrentRefresh(version)) {
288 return [];
289 }
291 > const plugins: IAgentPlugin[] = [];
292 > const seenPluginUris = new Set<string>();
293 > const attemptedPluginUris = new Set<string>();
294 >
295 > for (const source of sources) {
296 > const key = source.uri.toString();
297 > if (!attemptedPluginUris.has(key)) {
298 > attemptedPluginUris.add(key);
299 > try {
300 > const format = await detectPluginFormat(source.uri, this._fileService);
301 > if (!this._isCurrentRefresh(version)) {
302 return [];
303 }
304 > const plugin = await this._toPlugin(source.uri, format, source.fromMarketplace, source.repositoryUri, source.remove, version); agentPluginServiceImpl.ts
305 > seenPluginUris.add(key);
306 > plugins.push(plugin);
307 > } catch (error) {
308 this._logService.warn(`[AgentPluginDiscovery] Rejected plugin '${source.uri.toString()}': ${error instanceof Error ? error.message : String(error)}`);
309 }
311 > }
312 >
313 > if (this._isCurrentRefresh(version)) {
314 > this._disposePluginEntriesExcept(seenPluginUris);
315 > }
316 >
317 > plugins.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString()));
318 > return plugins;
319 > }
320
321 private _isCurrentRefresh(version: number): boolean {
322 > return version === this._discoverVersion && !this._store.isDisposed; agentPluginServiceImpl.ts
323 > }
324
325 protected async _pathExists(resource: URI): Promise<boolean> {
333
334 private async _toPlugin(uri: URI, format: IPluginFormatConfig, fromMarketplace: IMarketplacePlugin | undefined, repositoryUri: URI | undefined, removeCallback: (() => void) | undefined, version: number): Promise<IAgentPlugin> {
335 > const key = uri.toString(); agentPluginServiceImpl.ts
336 > const existing = this._pluginEntries.get(key);
337 > if (existing) {
338 if (!this._isCurrentRefresh(version)) {
339 return existing.plugin;
347 }
348 }
350 > const store = new DisposableStore();
351 > // Set by the service when enterprise policy blocks this plugin; when set,
352 > // the plugin is forced disabled regardless of the user's enablement choice.
353 > const policyBlocked = observableValue<boolean>('policyBlocked', false);
354 > const enablement = derived(r => policyBlocked.read(r)
355 ? ContributionEnablementState.DisabledProfile
356 > : this._enablementModel.readEnabled(key, r)); agentPluginServiceImpl.ts
357 >
358 > // Read the manifest up front so its `name` field can be used in the
359 > // plugin label (for direct installs that have no marketplace metadata).
360 > // Component directories are tracked via observers downstream and
361 > // re-read whenever the manifest changes on disk.
362 > const initialManifest = await readPluginManifest(uri, format, this._fileService);
363 > const manifest = observableValue<IPluginManifest | undefined>('agentPluginManifest', initialManifest);
364 >
365 > const observeComponent = <T>(
366 > prop: PluginComponent,
367 > doRead: (uris: readonly URI[]) => Promise<readonly T[]>,
368 > tryReadEmbedded?: (section: unknown) => Promise<T[] | undefined>,
369 > defaultPath: string = prop,
370 > ): IObservable<readonly T[]> => {
371 > const secondObs = derivedOpts({ equalsFn: equals }, reader => getPluginManifestComponent(format, prop, manifest.read(reader)));
372 >
373 > const wrapped = derived(reader => {
374 > if (format.requiresManifest && !manifest.read(reader)) {
375 return { kind: 'dirs', dirs: [] } as const;
376 }
377 > const section = secondObs.read(reader); agentPluginServiceImpl.ts
378 > if (tryReadEmbedded) {
379 > if (section && typeof section === 'object' && !Array.isArray(section) && !(hasKey(section, { paths: true }))) {
380 return { kind: 'const', data: new ObservablePromise(tryReadEmbedded(section)) } as const;
381 }
383 >
384 > const dirs = resolvePluginComponentDirs(uri, format, prop, defaultPath, section, repositoryUri);
385 > for (const d of dirs) {
386 > const watcher = this._fileService.createWatcher(d, { recursive: false, excludes: [] });
387 > reader.store.add(watcher);
388 > reader.store.add(watcher.onDidChange(() => changeTrigger.trigger(undefined)));
389 > }
390 >
391 > return { kind: 'dirs', dirs: dirs } as const;
392 > });
393 >
394 > const changeTrigger = observableSignal('fileChange');
395 >
396 > const promised = derived(reader => {
397 > const w = wrapped.read(reader);
398 > if (w.kind === 'const') {
399 return w.data.promiseResult;
400 > } else { agentPluginServiceImpl.ts
401 > changeTrigger.read(reader); // re-run when a relevant file change occurs
402 > const promise = new ObservablePromise(doRead(w.dirs));
403 > return promise.promiseResult;
404 > }
405 > });
406 >
407 > const result = promised.map((w, r) => w.read(r)?.data ?? Iterable.empty());
408 >
409 > return result.recomputeInitiallyAndOnChange(store);
410 > };
411 >
412 > const manifestUri = joinPath(uri, format.manifestPath);
413 > const commands = observeComponent('commands', d => readMarkdownComponents(d, this._fileService));
414 > const skills = observeComponent('skills', d => readPluginSkills(uri, d, format, this._fileService));
415 > const agents = observeComponent('agents', d => readMarkdownComponents(d, this._fileService));
416 > const instructions = observeComponent('rules', d => this._readRules(d));
417 > const hooks = observeComponent(
418 > 'hooks',
419 > paths => this._readHooksFromPaths(uri, paths, format),
420 > async section => {
421 const userHome = await this._pathService.userHome();
422 const workspaceRoot = resolveWorkspaceRoot(uri, this._workspaceContextService);
423 return toAgentPluginHooks(format.parseHooks(manifestUri, section, uri, workspaceRoot, userHome));
424 },
425 > format.hookConfigPath, agentPluginServiceImpl.ts
426 > );
427 >
428 > const mcpServerDefinitions = observeComponent(
429 > 'mcpServers',
430 > paths => readPluginMcpServers(uri, paths, format, this._fileService),
431 > async section => parseMcpServerDefinitionMap(manifestUri, { mcpServers: section }, uri.fsPath, format),
432 > '.mcp.json',
433 > );
434 >
435 > // Re-read the manifest whenever it changes on disk. The initial value
436 > // was already populated above before constructing the observable.
437 > const readManifest = async () => {
438 try {
439 const latestFormat = await detectPluginFormat(uri, this._fileService);
448 }
449 };
451 > const agentManifestUri = joinPath(uri, 'plugin.json');
452 > const rootWatcher = this._fileService.createWatcher(uri, { recursive: false, excludes: [] });
453 > store.add(rootWatcher);
454 > store.add(rootWatcher.onDidChange(change => {
455 if (change.affects(agentManifestUri)) {
456 void readManifest();
457 }
459 > store.add(this._fileService.onDidRunOperation(event => {
460 if (isEqual(event.resource, agentManifestUri)) {
461 void readManifest();
462 }
464 > if (!isEqual(manifestUri, agentManifestUri)) {
465 const manifestWatcher = this._fileService.createWatcher(manifestUri, { recursive: false, excludes: [] });
466 store.add(manifestWatcher);
467 store.add(manifestWatcher.onDidChange(() => readManifest()));
468 }
470 > const manifestName = typeof initialManifest?.name === 'string' && initialManifest.name.trim()
471 ? initialManifest.name.trim()
472 : undefined;
474 > const plugin: PluginEntry = {
475 > uri,
476 > format: format.format,
477 > label: fromMarketplace?.name ?? manifestName ?? basename(uri),
478 > enablement,
479 > policyBlocked,
480 > remove: removeCallback,
481 > hooks,
482 > commands,
483 > skills,
484 > agents,
485 > instructions,
486 > mcpServerDefinitions,
487 > fromMarketplace,
488 > };
489 >
490 > if (this._isCurrentRefresh(version)) {
491 > this._pluginEntries.set(key, { store, plugin, format });
492 > } else {
493 store.dispose();
494 }
496 > return plugin;
497 > }
498
499 /**
533 */
534 private async _readRules(dirs: readonly URI[]): Promise<readonly IAgentPluginInstruction[]> {
535 > const seen = new Set<string>(); agentPluginServiceImpl.ts
536 > const items: IAgentPluginInstruction[] = [];
537 >
538 > const matchSuffix = (filename: string): string | undefined => {
539 const lower = filename.toLowerCase();
540 return RULE_FILE_SUFFIXES.find(s => lower.endsWith(s));
541 };
543 > const addItem = (name: string, uri: URI) => {
544 if (!seen.has(name)) {
545 seen.add(name);
578 }
579 }
581 > items.sort((a, b) => a.name.localeCompare(b.name));
582 > return items;
583 > }
584
585 private _disposePluginEntriesExcept(keep: Set<string>): void {
586 for (const [key, entry] of this._pluginEntries) {
587 > if (!keep.has(key)) { agentPluginServiceImpl.ts
588 > entry.store.dispose();
589 > this._pluginEntries.delete(key);
590 > }
591 > }
592 }
593