agentPluginManager.ts ×19

Frontier kind: Code frontier

unlabeled · c_d5b497b71ddc

12 tests · 18417 LOC · 61 files · introduces 0 tests · 198 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
24 ranges198 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2033 ranges18417 lines · 61 files · Browse complete extent
All tests (intent)
12 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.

2 files ranked by introduced lines: 198 introduced LOC across 24 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/agentPluginManager.ts 180 introduced LOC · 19 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- agentPluginManager.ts
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 { VSBuffer } from '../../../base/common/buffer.js';
7 > import { SequencerByKey } from '../../../base/common/async.js';
8 > import { URI } from '../../../base/common/uri.js';
9 > import { IFileService } from '../../files/common/files.js';
10 > import { ILogService } from '../../log/common/log.js';
11 > import { IAgentPluginManager, type ISyncedCustomization } from '../common/agentPluginManager.js';
12 > import { CustomizationLoadStatus, type ClientPluginCustomization, type PluginCustomization } from '../common/state/sessionState.js';
13 > import { toAgentClientUri } from '../common/agentClientUri.js';
14 >
15 > const DEFAULT_MAX_PLUGINS = 20;
16 >
17 > /** On-disk cache entry format. */
18 > interface ICacheEntry {
19 > readonly uri: string;
20 > readonly nonce: string;
21 > }
22 >
23 > /**
24 > * Implementation of {@link IAgentPluginManager}.
25 > *
26 > * Syncs plugin directories to local storage under
27 > * `{userDataPath}/agentPlugins/{key}/{nonce}/`. Materializing each nonce in
28 > * its own subdirectory means a new revision is copied into a fresh directory
29 > * rather than overwriting (and deleting) the previous one. This both avoids
30 > * `EBUSY` failures when the in-use copy is still locked and allows multiple
31 > * revisions of the same plugin to coexist — e.g. a long-running session may
32 > * still reference an older nonce that we cannot delete yet. Uses a
33 > * {@link SequencerByKey} per plugin URI so that concurrent syncs of the same
34 > * plugin are serialized and cannot clobber each other.
35 > *
36 > * Older nonces of a plugin are evicted opportunistically: when the manager
37 > * starts up and again after each fresh sync of the same plugin. If a stale
38 > * nonce directory cannot be removed (e.g. it is still locked), it is retained
39 > * in the LRU and retried on a later cleanup pass.
40 > *
41 > * The LRU (which records each plugin's URI and nonce) is persisted to a JSON
42 > * file in the base path so it survives process restarts.
43 > */
44 > export class AgentPluginManager implements IAgentPluginManager {
45 > declare readonly _serviceBrand: undefined;
46 >
47 > private readonly _basePath: URI;
48 > private readonly _cachePath: URI;
49 > private readonly _maxPlugins: number;
50 >
51 > /** Serializes concurrent sync operations per plugin URI. */
52 > private readonly _sequencer = new SequencerByKey<string>();
53 >
54 > /**
55 > * LRU of synced plugins, most recently used at the end. Each entry records
56 > * the plugin's original customization URI and the nonce materialized on
57 > * disk under `{key}/{nonce}`.
58 > */
59 > private readonly _lru: ICacheEntry[] = [];
60 >
61 > /** Whether the on-disk cache has been loaded. */
62 > private _cacheLoaded = false;
63 >
64 > constructor(
65 > userDataPath: URI,
66 > @IFileService private readonly _fileService: IFileService,
67 > @ILogService private readonly _logService: ILogService,
68 > maxPlugins: number = DEFAULT_MAX_PLUGINS,
69 > ) {
70 > this._basePath = URI.joinPath(userDataPath, 'agentPlugins');
71 > this._cachePath = URI.joinPath(this._basePath, 'cache.json');
72 > this._maxPlugins = maxPlugins;
73 > }
74 >
75 > get basePath(): URI {
76 return this._basePath;
77 }
79 > async syncCustomizations(
80 > clientId: string,
81 > customizations: ClientPluginCustomization[],
82 > progress?: (status: PluginCustomization) => void,
83 > ): Promise<ISyncedCustomization[]> {
84 > await this._ensureCacheLoaded();
85 >
86 > // Sync each customization in parallel, serialized per URI
87 > const results = await Promise.all(customizations.map(ref =>
88 > this._sequencer.queue(ref.uri, async (): Promise<ISyncedCustomization> => {
89 > try {
90 > const pluginDir = await this._syncPlugin(clientId, ref);
91 const customization: PluginCustomization = { ...ref, load: { kind: CustomizationLoadStatus.Loaded } };
92 progress?.(customization);
93 > return { customization, pluginDir }; agentPluginManager.ts
94 > } catch (err) {
95 const message = err instanceof Error ? err.message : String(err);
96 this._logService.error(`[AgentPluginManager] Failed to sync plugin ${ref.uri}: ${message}`);
99 return { customization };
100 }
102 > ));
103 >
104 > return results;
105 > }
106 >
107 > // ---- plugin storage logic -----------------------------------------------
108 >
109 > /**
110 > * Syncs a single plugin to local storage. Each nonce is materialized in its
111 > * own `{key}/{nonce}` subdirectory; when the same nonce is already present
112 > * the copy is skipped. After a fresh copy, older nonces of the same plugin
113 > * are evicted on a best-effort basis (retained in the LRU if still locked).
114 > * Returns the local directory URI.
115 > */
116 > private async _syncPlugin(clientId: string, ref: ClientPluginCustomization): Promise<URI> {
117 > const pluginUri = toAgentClientUri(URI.parse(ref.uri), clientId);
118 > const destDir = this._dirFor(ref.uri, ref.nonce);
119 >
120 > // Nonce cache hit — the plugin is already materialized under the nonce
121 > // subdirectory, so skip the copy.
122 > if (ref.nonce && this._findEntry(ref.uri, ref.nonce) && await this._fileService.exists(destDir)) {
123 this._touchLru(ref.uri, ref.nonce);
124 this._logService.trace(`[AgentPluginManager] Nonce match for ${ref.uri}, skipping copy`);
125 return destDir;
126 }
128 > this._logService.info(`[AgentPluginManager] Syncing plugin: ${ref.uri}${destDir.toString()}`);
129 >
130 > await this._fileService.copy(pluginUri, destDir, true);
131
132 this._removeEntry(ref.uri, ref.nonce);
133 > this._lru.push({ uri: ref.uri, nonce: ref.nonce ?? '' }); agentPluginManager.ts
134 >
135 > // Try to clean up superseded nonces of this plugin; undeletable ones stay
136 > // in the LRU for a later attempt.
137 > await this._cleanupStaleNoncesFor(ref.uri);
138 await this._evictIfNeeded();
139 await this._persistCache();
140
141 return destDir;
143 >
144 > private _keyForUri(uri: string): string {
145 > return this._sanitize(uri);
146 > }
147 >
148 > private _keyForNonce(nonce: string | undefined): string {
149 > return (nonce && this._sanitize(nonce)) || 'default';
150 > }
151 >
152 > private _sanitize(value: string): string {
153 > return value.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 128);
154 > }
155 >
156 > /** Directory in which a specific `(uri, nonce)` revision is materialized. */
157 > private _dirFor(uri: string, nonce: string | undefined): URI {
158 > return URI.joinPath(this._basePath, this._keyForUri(uri), this._keyForNonce(nonce));
159 > }
160 >
161 > /** Parent directory holding all materialized nonces of a plugin. */
162 > private _pluginRootFor(uri: string): URI {
163 return URI.joinPath(this._basePath, this._keyForUri(uri));
164 }
166 > private _findEntry(uri: string, nonce: string | undefined): ICacheEntry | undefined {
167 const n = nonce ?? '';
168 return this._lru.find(entry => entry.uri === uri && entry.nonce === n);
169 }
171 > private _removeEntry(uri: string, nonce: string | undefined): void {
172 const entry = this._findEntry(uri, nonce);
173 if (entry) {
175 }
176 }
178 > private _removeEntryRef(entry: ICacheEntry): void {
179 const idx = this._lru.indexOf(entry);
180 if (idx !== -1) {
182 }
183 }
185 > private _touchLru(uri: string, nonce: string | undefined): void {
186 const entry = this._findEntry(uri, nonce);
187 if (entry) {
190 }
191 }
193 > /** Best-effort recursive delete; returns `true` only when the dir is gone. */
194 > private async _tryDeleteDir(dir: URI): Promise<boolean> {
195 try {
196 await this._fileService.del(dir, { recursive: true });
201 }
202 }
204 > /** Attempts to evict older nonces of every tracked plugin. */
205 > private async _cleanupStaleNonces(): Promise<void> {
206 for (const uri of new Set(this._lru.map(entry => entry.uri))) {
207 await this._cleanupStaleNoncesFor(uri);
208 }
209 }
211 > /**
212 > * Attempts to evict every nonce of {@link uri} except the most recently used
213 > * one. Entries whose directory cannot be removed are left in the LRU so they
214 > * can be retried later, once whatever was holding them has released them.
215 > */
216 > private async _cleanupStaleNoncesFor(uri: string): Promise<void> {
217 const entries = this._lru.filter(entry => entry.uri === uri);
218 // `entries` preserves LRU order; the last is the current revision.
225 }
226 }
228 > private async _evictIfNeeded(): Promise<void> {
229 // Pop from the head until we're at-or-below the cap. Entries whose
230 // directory can't be deleted (still locked by a running session)
246 }
247 }
249 > // ---- cache persistence --------------------------------------------------
250 >
251 > private async _ensureCacheLoaded(): Promise<void> {
252 > if (this._cacheLoaded) {
253 return;
254 }
255 > this._cacheLoaded = true; agentPluginManager.ts
256 >
257 > try {
258 > if (!await this._fileService.exists(this._cachePath)) {
259 > return;
260 > }
261 const content = await this._fileService.readFile(this._cachePath);
262 const entries: ICacheEntry[] = JSON.parse(content.value.toString());
278 await this._cleanupStaleNonces();
279 await this._persistCache();
281 >
282 > private async _persistCache(): Promise<void> {
283 try {
284 // Write entries in LRU order (oldest first)
src/vs/platform/files/common/fileService.ts 18 introduced LOC · 5 ranges

Open complete file

802
803 async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
804 > const sourceProvider = await this.withReadProvider(source); fileService.ts
805 > const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target);
806 >
807 > // copy
808 > const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite);
809
810 // resolve and send events
811 const fileStat = await this.resolve(target, { resolveMetadata: true });
812 > this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat)); fileService.ts
813 >
814 > return fileStat;
815 > }
816
817 private async doMoveCopy(sourceProvider: IFileSystemProvider, source: URI, targetProvider: IFileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite: boolean): Promise<'move' | 'copy'> {
833 // copy source => target
834 if (mode === 'copy') {
836 > // same provider with fast copy: leverage copy() functionality
837 > if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) {
838 await sourceProvider.copy(source, target, { overwrite });
839 }
841 > // when copying via buffer/unbuffered, we have to manually
842 > // traverse the source if it is a folder and not a file
843 > else {
844 > const sourceFile = await this.resolve(source);
845 if (sourceFile.isDirectory) {
846 await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target);
848 await this.doCopyFile(sourceProvider, source, targetProvider, target);
849 }
850 > } fileService.ts
851
852 return mode;