src/vs/platform/agentHost/common/agentHostFileSystemProvider.ts

649 LOC · 566 covered · 83 uncovered · 121 ranges · 1670 concepts · 53 introducers · 852 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

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 related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentHostFileSystemProvider.ts ×25
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 { decodeBase64, VSBuffer } from '../../../base/common/buffer.js';
7 > import { disposableTimeout } from '../../../base/common/async.js';
8 > import { Emitter, Event } from '../../../base/common/event.js';
9 > import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10 > import { URI } from '../../../base/common/uri.js';
11 > import { createFileSystemProviderError, FileChangeType, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProvider, IFileSystemProviderWithFileRealpathCapability, IFileWriteOptions, IStat, IWatchOptions } from '../../files/common/files.js';
12 > import { fromAgentHostUri, toAgentHostUri } from './agentHostUri.js';
13 > import { ContentEncoding, type CreateResourceWatchParams, type DirectoryEntry, type ResourceCopyParams, type ResourceCopyResult, type ResourceDeleteParams, type ResourceDeleteResult, type ResourceListResult, type ResourceMkdirParams, type ResourceMkdirResult, type ResourceMoveParams, type ResourceMoveResult, type ResourceReadResult, type ResourceRequestParams, type ResourceRequestResult, type ResourceResolveParams, type ResourceResolveResult, type ResourceWriteParams, type ResourceWriteResult } from './state/protocol/commands.js';
14 > import { AhpErrorCodes } from './state/protocol/errors.js';
15 > import { ProtocolError } from './state/sessionProtocol.js';
16 > import { ActionType, type ActionEnvelope } from './state/sessionActions.js';
17 > import { ROOT_STATE_URI } from './state/sessionState.js';
18 >
19 > /**
20 > * Interface for performing resource operations on a remote endpoint.
21 > *
22 > * Both {@link IAgentConnection} (client→server) and client-exposed
23 > * filesystems (server→client) satisfy this contract.
24 > */
25 > export interface IRemoteFilesystemConnection {
26 > resourceList(uri: URI): Promise<ResourceListResult>;
27 > resourceRead(uri: URI): Promise<ResourceReadResult>;
28 > resourceWrite(params: ResourceWriteParams): Promise<ResourceWriteResult>;
29 > resourceDelete(params: ResourceDeleteParams): Promise<ResourceDeleteResult>;
30 > resourceMove(params: ResourceMoveParams): Promise<ResourceMoveResult>;
31 > /** Copy a resource on the remote endpoint. */
32 > resourceCopy(params: ResourceCopyParams): Promise<ResourceCopyResult>;
33 > /**
34 > * Negotiate access to a resource the receiver mediates. Optional because
35 > * not every connection in the codebase carries one — only the agent-host
36 > * server-to-client direction needs to send `resourceRequest` today.
37 > */
38 > resourceRequest?(params: ResourceRequestParams): Promise<ResourceRequestResult>;
39 > /** Resolve (stat + realpath) a resource on the remote endpoint. */
40 > resourceResolve(params: ResourceResolveParams): Promise<ResourceResolveResult>;
41 > /** Create a directory on the remote endpoint (mkdir -p semantics). */
42 > resourceMkdir(params: ResourceMkdirParams): Promise<ResourceMkdirResult>;
43 > /**
44 > * Start a file-system watcher on the remote endpoint and return a
45 > * handle whose `onDidChange` event fires for every change the remote
46 > * reports under the watched root. Disposing the handle unsubscribes
47 > * the watch (subject to the receiver's grace window).
48 > *
49 > * Optional: implementations without subscription machinery omit it; the
50 > * filesystem provider degrades to a no-op `watch()` in that case.
51 > */
52 > watchResource?(params: CreateResourceWatchParams): Promise<IRemoteWatchHandle>;
53 > }
54 >
55 > /**
56 > * Handle for a remote file-system watcher returned by
57 > * {@link IRemoteFilesystemConnection.watchResource}. Mirrors the shape
58 > * of `IFileSystemWatcher` from `../../files/common/files.js` so the FS
59 > * provider can plug events straight into its own `onDidChangeFile`
60 > * emitter.
61 > */
62 > export interface IRemoteWatchHandle extends IDisposable {
63 > readonly onDidChange: Event<readonly IFileChange[]>;
64 > }
65 >
66 > /**
67 > * Shared implementation of {@link IAgentConnection.watchResource} —
68 > * bundles `createResourceWatch` + `subscribe` + a per-channel listener
69 > * on the action stream into an {@link IRemoteWatchHandle}. Used by
70 > * every transport that exposes those four primitives so we don't need
71 > * to duplicate the wire bookkeeping in each `IAgentConnection`
72 > * implementation.
73 > */
74 export async function createRemoteWatchHandle(
75 primitives: {
76 createResourceWatch(params: CreateResourceWatchParams): Promise<{ channel: string }>;
77 subscribe(channel: URI): Promise<unknown>;
78 unsubscribe(channel: URI): void;
79 onDidAction: Event<ActionEnvelope>;
80 },
81 params: CreateResourceWatchParams,
82 ): Promise<IRemoteWatchHandle> {
83 const { channel } = await primitives.createResourceWatch(params);
84 const channelUri = URI.parse(channel);
85 await primitives.subscribe(channelUri);
86 const onDidChangeEmitter = new Emitter<readonly IFileChange[]>();
87 const listener = primitives.onDidAction(envelope => {
88 if (envelope.channel !== channel || envelope.action.type !== ActionType.ResourceWatchChanged) {
89 return;
90 }
91 const items = envelope.action.changes?.items ?? [];
92 if (items.length === 0) {
93 return;
94 }
95 onDidChangeEmitter.fire(items.map(item => ({
96 resource: URI.parse(item.uri),
97 type: item.type === 'added' ? FileChangeType.ADDED
98 : item.type === 'deleted' ? FileChangeType.DELETED
99 : FileChangeType.UPDATED,
100 })));
101 });
102 let disposed = false;
103 return {
104 onDidChange: onDidChangeEmitter.event,
105 dispose: () => {
106 if (disposed) {
107 return;
108 }
109 disposed = true;
110 listener.dispose();
111 onDidChangeEmitter.dispose();
112 try {
113 primitives.unsubscribe(channelUri);
114 } catch {
115 // Connection may already be gone; the server-side grace
116 // timer will clean up.
117 }
118 },
119 };
120 }
122 > /**
123 > * Build a {@link AGENT_HOST_SCHEME} URI for a given connection authority
124 > * and remote path. Assumes the remote path is a `file://` resource.
125 > */
126 > export function agentHostUri(authority: string, path: string): URI {
127 > return toAgentHostUri(URI.file(path), authority); agentHostFileSystemProvider.ts ×1
128 > }
130 > /**
131 > * Extract the remote filesystem path from a {@link AGENT_HOST_SCHEME} URI.
132 > */
133 > export function agentHostRemotePath(uri: URI): string {
134 > return fromAgentHostUri(uri).path; agentHostFileSystemProvider.ts ×1
135 > }
137 > // ---- Abstract base ----------------------------------------------------------
138 >
139 > interface IAuthorityEntry {
140 > /**
141 > * All currently-registered connections for this authority, oldest
142 > * first. The active connection is the last entry (most recent
143 > * registration wins). Older registrations are kept so that if a
144 > * caller registers `A`, then `B`, then disposes `B`, we transparently
145 > * fall back to `A` instead of entering a grace window.
146 > *
147 > * Empty while the entry is inside the grace window.
148 > */
149 > connections: IRemoteFilesystemConnection[];
150 > /**
151 > * Pending eviction timer; armed while {@link connections} is empty,
152 > * cleared on re-registration or eviction.
153 > */
154 > readonly expiry: MutableDisposable<IDisposable>;
155 > }
156 >
157 > /**
158 > * {@link IFileSystemProvider} that proxies filesystem operations
159 > * through a {@link IRemoteFilesystemConnection}.
160 > *
161 > * URIs encode the original scheme and authority in the path so any remote
162 > * resource can be represented. Subclasses provide the URI decode function
163 > * and scheme-specific helpers.
164 > *
165 > * Individual connections are identified by the URI's authority component.
166 > */
167 > export abstract class AHPFileSystemProvider extends Disposable implements IFileSystemProvider, IFileSystemProviderWithFileRealpathCapability {
168 >
169 > readonly capabilities =
170 > FileSystemProviderCapabilities.PathCaseSensitive |
171 > FileSystemProviderCapabilities.FileReadWrite |
172 > FileSystemProviderCapabilities.FileFolderCopy |
173 > FileSystemProviderCapabilities.FileRealpath;
174 >
175 > private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
176 > readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
177 >
178 > private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
179 > readonly onDidChangeFile = this._onDidChangeFile.event;
180 > private readonly _onDidWatchError = this._register(new Emitter<string>());
181 > readonly onDidWatchError = this._onDidWatchError.event;
182 >
183 > /**
184 > * Per-authority registration slot. We keep the slot alive for a brief
185 > * grace period after the last registration is disposed, so an
186 > * operation issued during a reconnection window can wait for the
187 > * replacement registration instead of failing immediately.
188 > */
189 > private readonly _authorities = new Map<string, IAuthorityEntry>();
190 >
191 > /**
192 > * Fires the authority whose active connection has changed: added,
193 > * replaced, fallen back to an older registration, entered the grace
194 > * window (no active connection), or evicted. Long-lived consumers
195 > * (e.g. {@link watch}) subscribe here so they continue to receive
196 > * notifications across full entry eviction + later re-creation —
197 > * something a per-entry emitter cannot offer.
198 > */
199 > private readonly _onDidChangeConnection = this._register(new Emitter<string>());
200 >
201 > /**
202 > * Grace period during which {@link _getConnection} will await a new
203 > * registration after the previous one is disposed. Covers the window
204 > * where a transport is briefly torn down and re-registered (e.g. an
205 > * agent-host client reconnect that races a plugin sync). 5s matches
206 > * the typical reconnect timeout. Consumers should still implement
207 > * logical retries for longer reconnection latencies, but this is a
208 > * low level, best-effort mechanism.
209 > *
210 > * Tests can override this via the constructor parameter.
211 > */
212 > private static readonly _DEFAULT_CONNECTION_GRACE_MS = 5000;
213 >
214 > constructor(
215 > private readonly _connectionGraceMs: number = AHPFileSystemProvider._DEFAULT_CONNECTION_GRACE_MS, agentHostFileSystemProvider.ts ×3
216 > ) {
217 > super();
218 > }
220 > /**
221 > * Register a mapping from a URI authority to a connection.
222 > * Returns a disposable that unregisters the mapping. Multiple
223 > * concurrent registrations for the same authority are supported;
224 > * the most recent registration wins, and disposing it falls back to
225 > * the previous one (if any). After the *last* registration is
226 > * disposed the entry is held open for {@link _connectionGraceMs} so
227 > * that a reconnect can replace it without orphaning in-flight
228 > * operations.
229 > */
230 > registerAuthority(authority: string, connection: IRemoteFilesystemConnection): IDisposable {
231 > let entry = this._authorities.get(authority); agentHostFileSystemProvider.ts ×3
232 > if (!entry) {
233 > entry = {
234 > connections: [connection],
235 > expiry: new MutableDisposable<IDisposable>(),
236 > };
237 > this._authorities.set(authority, entry);
238 > } else {
239 > entry.expiry.clear(); agentHostFileSystemProvider.ts ×1
240 > entry.connections.push(connection);
241 > }
242 > const adopted = entry; agentHostFileSystemProvider.ts ×3
243 > this._onDidChangeConnection.fire(authority);
244 >
245 > return toDisposable(() => {
246 > const idx = adopted.connections.indexOf(connection);
247 > if (idx === -1) {
249 > }
250 > const wasActive = idx === adopted.connections.length - 1; agentHostFileSystemProvider.ts ×2
251 > adopted.connections.splice(idx, 1);
252 > if (adopted.connections.length === 0) {
253 > adopted.expiry.value = disposableTimeout( agentHostFileSystemProvider.ts ×1
254 > () => this._expireAuthority(authority, adopted),
255 > this._connectionGraceMs,
256 > this._store,
257 > );
258 > }
260 > if (wasActive) {
261 > this._onDidChangeConnection.fire(authority); // Falling back to an older connection — surface the change. agentHostFileSystemProvider.ts ×1
262 > }
264 > }
266 > private _expireAuthority(authority: string, entry: IAuthorityEntry): void {
267 > // A re-registration may have landed between scheduling and agentHostFileSystemProvider.ts ×2
268 > // firing — bail in that case.
269 > if (this._authorities.get(authority) !== entry || entry.connections.length > 0) {
270 return;
271 }
272 > this._authorities.delete(authority); agentHostFileSystemProvider.ts ×2
273 > entry.expiry.dispose();
274 > this._onDidChangeConnection.fire(authority);
275 > }
277 > override dispose(): void {
278 > for (const entry of this._authorities.values()) { agentHostFileSystemProvider.ts ×3
279 > entry.expiry.dispose(); agentHostFileSystemProvider.ts ×1
280 > entry.connections.length = 0;
281 > }
282 > this._authorities.clear(); agentHostFileSystemProvider.ts ×3
283 > super.dispose();
284 > }
286 > /** Decode a provider URI back to the original URI for the remote endpoint. */
287 > protected abstract _decodeUri(resource: URI): URI;
288 >
289 > /** Encode a remote URI back into a provider URI with the given authority. */
290 > protected abstract _encodeUri(resource: URI, authority: string): URI;
291 >
292 > watch(resource: URI, opts: IWatchOptions): IDisposable {
293 > // `IFileSystemProvider.watch` is synchronous, but acquiring a agentHostFileSystemProvider.ts ×11
294 > // connection may have to wait for a (re)registration and the
295 > // underlying AHP `createResourceWatch` + `subscribe` round-trip
296 > // is itself async. Additionally, watchers are long-lived: every
297 > // time the active connection for `authority` changes (reconnect,
298 > // fallback to an older registration, eviction followed by a fresh
299 > // registration, ...) we tear down any existing remote handle and
300 > // re-attach against the new connection. The class-level
301 > // {@link _onDidChangeConnection} event keeps us informed across
302 > // the full entry-eviction cycle.
303 > const store = new DisposableStore();
304 > const handleHolder = store.add(new MutableDisposable<IDisposable>());
305 > const authority = resource.authority;
306 > const params: CreateResourceWatchParams = {
307 > channel: ROOT_STATE_URI,
308 > uri: this._decodeUri(resource).toString(),
309 > recursive: opts.recursive,
310 > ...(opts.excludes.length > 0 ? { excludes: { items: [...opts.excludes] } } : {}),
311 > ...(opts.includes && opts.includes.length > 0
312 > ? { includes: { items: opts.includes.map(p => typeof p === 'string' ? p : p.pattern) } } agentHostFileSystemProvider.ts ×1
315 >
316 > // Track which connection the current handle was created against
317 > // so we ignore spurious change events that don't represent a
318 > // real swap (e.g. a stale registration disposal).
319 > let attached: IRemoteFilesystemConnection | undefined;
320 > let attaching = false;
321 > let pendingReattach = false;
322 >
323 > const reattach = async (): Promise<void> => {
324 > if (store.isDisposed) {
325 return;
326 }
327 > if (attaching) { agentHostFileSystemProvider.ts ×11
328 pendingReattach = true;
329 return;
330 }
331 > const entry = this._authorities.get(authority); agentHostFileSystemProvider.ts ×11
332 > const next = entry?.connections.at(-1);
333 > if (next === attached) {
335 > }
336 > handleHolder.clear(); agentHostFileSystemProvider.ts ×11
337 > attached = undefined;
338 > const watchResource = next?.watchResource;
339 > if (!next || !watchResource) {
341 > }
342 > attaching = true; agentHostFileSystemProvider.ts ×11
343 > const target = next;
344 > try {
345 > const handle = await watchResource.call(target, params);
346 > if (store.isDisposed) { agentHostFileSystemProvider.ts ×4
347 handle.dispose();
348 return;
349 }
350 > const current = this._authorities.get(authority); agentHostFileSystemProvider.ts ×4
351 > if (!current || current.connections.at(-1) !== target) { agentHostFileSystemProvider.ts ×11
352 // Active connection changed underneath us — toss this
353 // handle and let the pending reattach pick the new one.
354 handle.dispose();
355 return;
356 }
357 > const sub = handle.onDidChange(changes => this._onDidChangeFile.fire(changes.map(c => ({ agentHostFileSystemProvider.ts ×4
358 > resource: this._encodeUri(c.resource, resource.authority), agentHostFileSystemProvider.ts ×1
359 > type: c.type,
361 > handleHolder.value = toDisposable(() => {
362 > sub.dispose();
363 > handle.dispose();
364 > });
365 > attached = target;
366 > } catch (err) { agentHostFileSystemProvider.ts ×11
367 > this._onDidWatchError.fire(err instanceof Error ? err.message : String(err)); agentHostFileSystemProvider.ts ×1
369 > attaching = false;
370 > if (pendingReattach) {
371 pendingReattach = false;
372 void reattach();
373 }
375 > };
376 >
377 > store.add(this._onDidChangeConnection.event(a => {
378 > if (a === authority) { agentHostFileSystemProvider.ts ×1
379 > void reattach();
380 > }
382 > void reattach();
383 >
384 > return store;
385 > }
387 > async stat(resource: URI): Promise<IStat> {
388 > const path = resource.path; agentHostFileSystemProvider.ts ×5
389 >
390 > if (path === '/' || path === '') {
391 return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
392 }
393 > const decoded = this._decodeUri(resource); agentHostFileSystemProvider.ts ×5
394 > if (decoded.scheme === 'session-db' || decoded.scheme === 'git-blob') {
395 > return { type: FileType.File, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly }; agentHostFileSystemProvider.ts ×1
396 > }
398 > if (decoded.path === '/' || decoded.path === '') { agentHostFileSystemProvider.ts ×5
399 return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
400 }
402 > const connection = await this._getConnection(resource.authority);
403 > try {
404 > const resolved = await this._resolve(connection, decoded);
405 >
406 > return {
407 > type: resolved.type === 'directory' ? FileType.Directory
408 > : resolved.type === 'symlink' ? FileType.SymbolicLink agentHostFileSystemProvider.ts ×1
409 > : FileType.File,
410 > mtime: resolved.mtime ? Date.parse(resolved.mtime) : 0, agentHostFileSystemProvider.ts ×5
411 > ctime: resolved.ctime ? Date.parse(resolved.ctime) : 0,
412 > size: resolved.size ?? 0,
413 > };
414 > } catch (err) {
415 throw this._mapError(err, FileSystemProviderErrorCode.FileNotFound);
416 }
419 > async realpath(resource: URI): Promise<string> {
420 > const path = resource.path; agentHostFileSystemProvider.ts ×4
421 > // Synthetic roots and virtual content schemes have no distinct
422 > // canonical path — return the input path unchanged.
423 > if (path === '/' || path === '') {
424 return path;
425 }
426 > const decoded = this._decodeUri(resource); agentHostFileSystemProvider.ts ×4
427 > if (decoded.scheme === 'session-db' || decoded.scheme === 'git-blob' || decoded.path === '/' || decoded.path === '') {
428 return path;
429 }
430 > const connection = await this._getConnection(resource.authority); agentHostFileSystemProvider.ts ×4
431 > try {
432 > const resolved = await this._resolve(connection, decoded);
433 > // `resolved.uri` is the remote canonical (realpath) URI. Re-encode
434 > // it back into provider space; the file service applies the
435 > // returned path onto the original provider URI.
436 > return this._encodeUri(URI.parse(resolved.uri), resource.authority).path;
437 > } catch (err) {
438 throw this._mapError(err, FileSystemProviderErrorCode.FileNotFound);
439 }
442 > async readdir(resource: URI): Promise<[string, FileType][]> {
443 > const entries = await this._listDirectory(resource.authority, resource); agentHostFileSystemProvider.ts ×4
444 > return entries.map(e => [e.name, e.type === 'directory' ? FileType.Directory : FileType.File]); agentHostFileSystemProvider.ts ×2
447 > async readFile(resource: URI): Promise<Uint8Array> {
448 > const connection = await this._getConnection(resource.authority); agentHostFileSystemProvider.ts ×3
449 > try {
450 > const originalUri = this._decodeUri(resource);
451 > const result = await connection.resourceRead(originalUri);
452 > if (result.encoding === ContentEncoding.Base64) { agentHostFileSystemProvider.ts ×2
453 return decodeBase64(result.data).buffer;
454 }
455 > return VSBuffer.fromString(result.data).buffer; agentHostFileSystemProvider.ts ×2
456 > } catch (err) { agentHostFileSystemProvider.ts ×3
457 > throw this._mapError(err, FileSystemProviderErrorCode.FileNotFound); agentHostFileSystemProvider.ts ×1
458 > }
461 > async writeFile(resource: URI, content: Uint8Array, _opts: IFileWriteOptions): Promise<void> {
462 > const connection = await this._getConnection(resource.authority); agentHostFileSystemProvider.ts ×3
463 > try {
464 > const originalUri = this._decodeUri(resource);
465 > await connection.resourceWrite({
466 > channel: ROOT_STATE_URI,
467 > uri: originalUri.toString(),
468 > data: VSBuffer.wrap(content).toString(),
469 > encoding: ContentEncoding.Utf8,
470 > });
471 > } catch (err) {
472 > throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions);
473 > }
474 > }
476 > async mkdir(resource: URI): Promise<void> {
477 > const connection = await this._getConnection(resource.authority); agentHostFileSystemProvider.ts ×2
478 > try {
479 > const originalUri = this._decodeUri(resource);
480 > await connection.resourceMkdir({ channel: ROOT_STATE_URI, uri: originalUri.toString() });
481 > } catch (err) {
482 throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions);
483 }
486 > async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
487 > const connection = await this._getConnection(resource.authority); agentHostFileSystemProvider.ts ×3
488 > try {
489 > const originalUri = this._decodeUri(resource);
490 > await connection.resourceDelete({ channel: ROOT_STATE_URI, uri: originalUri.toString(), recursive: opts.recursive });
491 > } catch (err) {
492 > throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions);
493 > }
494 > }
496 > async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
497 > const connection = await this._getConnection(from.authority); agentHostFileSystemProvider.ts ×3
498 > try {
499 > const originalFrom = this._decodeUri(from);
500 > const originalTo = this._decodeUri(to);
501 > await connection.resourceMove({ channel: ROOT_STATE_URI, source: originalFrom.toString(), destination: originalTo.toString(), failIfExists: !opts.overwrite });
502 > } catch (err) {
503 > throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions);
504 > }
505 > }
507 > async copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
508 > const connection = await this._getConnection(from.authority); agentHostFileSystemProvider.ts ×2
509 > try {
510 > const originalFrom = this._decodeUri(from);
511 > const originalTo = this._decodeUri(to);
512 > await connection.resourceCopy({ channel: ROOT_STATE_URI, source: originalFrom.toString(), destination: originalTo.toString(), failIfExists: !opts.overwrite });
513 > } catch (err) {
514 throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions);
515 }
518 > /**
519 > * Negotiate access to {@link resource} with the receiver, asking for the
520 > * granted modes in {@link opts}. Used after a `NoPermissions` failure to
521 > * prompt the receiver to grant access; the caller can then retry.
522 > *
523 > * Resolves on success. Rejects if the receiver denies, the connection
524 > * is missing, or the connection doesn't implement `resourceRequest`.
525 > */
526 > async requestResourceAccess(resource: URI, opts: { readonly read?: boolean; readonly write?: boolean }): Promise<void> {
527 > const connection = await this._getConnection(resource.authority); agentHostFileSystemProvider.ts ×2
528 > if (!connection.resourceRequest) {
529 > throw createFileSystemProviderError( agentHostFileSystemProvider.ts ×1
530 > `Connection for ${resource.authority} does not support resourceRequest`,
531 > FileSystemProviderErrorCode.Unavailable,
532 > );
533 > }
534 > const originalUri = this._decodeUri(resource); agentHostFileSystemProvider.ts ×1
535 > try {
536 > await connection.resourceRequest({
537 > channel: ROOT_STATE_URI,
538 > uri: originalUri.toString(),
539 > read: opts.read,
540 > write: opts.write,
541 > });
542 > } catch (err) {
543 > throw this._mapError(err, FileSystemProviderErrorCode.NoPermissions); agentHostFileSystemProvider.ts ×1
544 > }
547 > // ---- Internals ----------------------------------------------------------
548 >
549 > private _getConnection(authority: string): Promise<IRemoteFilesystemConnection> {
550 > const entry = this._authorities.get(authority); agentHostFileSystemProvider.ts ×2
551 > if (!entry) {
552 > return Promise.reject(createFileSystemProviderError( agentHostFileSystemProvider.ts ×1
553 > `No connection for authority: ${authority}`,
554 > FileSystemProviderErrorCode.Unavailable,
555 > ));
556 > }
558 > const active = entry.connections.at(-1);
559 > if (active) {
560 > return Promise.resolve(active); agentHostFileSystemProvider.ts ×1
561 > }
562 > // Entry is inside its grace window after the last registration agentHostFileSystemProvider.ts ×3
563 > // was disposed. Wait until either a new registration arrives
564 > // (resolve) or the grace timer expires and evicts the entry
565 > // (reject).
566 > return new Promise((resolve, reject) => {
567 > const settle = (): void => {
568 > const current = this._authorities.get(authority);
569 > if (!current) {
570 > sub.dispose(); agentHostFileSystemProvider.ts ×1
571 > reject(createFileSystemProviderError(
572 > `No connection for authority: ${authority}`,
573 > FileSystemProviderErrorCode.Unavailable,
574 > ));
575 > return;
576 > }
577 > const c = current.connections.at(-1); agentHostFileSystemProvider.ts ×3
578 > if (c) {
579 > sub.dispose(); agentHostFileSystemProvider.ts ×1
580 > resolve(c);
581 > }
583 > const sub = this._onDidChangeConnection.event(a => {
584 > if (a === authority) {
585 > settle();
586 > }
587 > });
588 > // Re-check after subscribing in case the state changed between
589 > // our initial check and the listener registration.
590 > settle();
591 > });
594 > /**
595 > * Translate a thrown error from a {@link IRemoteFilesystemConnection}
596 > * into a {@link FileSystemProviderError}. Preserves `PermissionDenied`
597 > * (-32009) as `NoPermissions` so callers can distinguish a
598 > * permission failure from `NotFound` and decide whether to negotiate
599 > * via {@link requestResourceAccess}.
600 > */
601 > private _mapError(err: unknown, defaultCode: FileSystemProviderErrorCode): Error {
602 > if (err instanceof ProtocolError && err.code === AhpErrorCodes.PermissionDenied) { agentHostFileSystemProvider.ts ×2
603 > return createFileSystemProviderError(err.message, FileSystemProviderErrorCode.NoPermissions); agentHostFileSystemProvider.ts ×1
604 > }
605 > return createFileSystemProviderError( agentHostFileSystemProvider.ts ×1
606 > err instanceof Error ? err.message : String(err), agentHostFileSystemProvider.ts ×2
607 > defaultCode,
608 > );
609 > }
611 > /**
612 > * Resolve a decoded resource over {@link connection}. Shared by
613 > * {@link stat} and {@link realpath}.
614 > */
615 > private _resolve(connection: IRemoteFilesystemConnection, decoded: URI): Promise<ResourceResolveResult> {
616 > return connection.resourceResolve({ channel: ROOT_STATE_URI, uri: decoded.toString() }); agentHostFileSystemProvider.ts ×1
617 > }
619 > private async _listDirectory(authority: string, resource: URI): Promise<readonly DirectoryEntry[]> {
620 > const connection = await this._getConnection(authority); agentHostFileSystemProvider.ts ×4
622 > const originalUri = this._decodeUri(resource);
623 > const result = await connection.resourceList(originalUri);
624 > return result.entries; agentHostFileSystemProvider.ts ×2
625 > } catch (err) { agentHostFileSystemProvider.ts ×2
626 > throw this._mapError(err, FileSystemProviderErrorCode.Unavailable); agentHostFileSystemProvider.ts ×1
627 > }
630 >
631 > // ---- Agent Host filesystem (client reads agent host files) ------------------
632 >
633 > /**
634 > * Filesystem provider for accessing agent host files from the
635 > * client side. Registered under the `vscode-agent-host` scheme.
636 > *
637 > * ```
638 > * vscode-agent-host://[connectionAuthority]/[originalScheme]/[originalAuthority]/[originalPath]
639 > * ```
640 > */
641 > export class AgentHostFileSystemProvider extends AHPFileSystemProvider {
642 > protected _decodeUri(resource: URI): URI {
643 > return fromAgentHostUri(resource); agentHostFileSystemProvider.ts ×1
644 > }
646 > protected _encodeUri(resource: URI, authority: string): URI {
647 > return toAgentHostUri(resource, authority); agentHostFileSystemProvider.ts ×1
648 > }