Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts
13401 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 { Codicon } from '../../../../base/common/codicons.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { MarkdownString } from '../../../../base/common/htmlContent.js';
9
import { DisposableStore } from '../../../../base/common/lifecycle.js';
10
import { Schemas } from '../../../../base/common/network.js';
11
import { basename, dirname } from '../../../../base/common/resources.js';
12
import { IObservable, observableValue } from '../../../../base/common/observable.js';
13
import { isWeb } from '../../../../base/common/platform.js';
14
import { ThemeIcon } from '../../../../base/common/themables.js';
15
import { URI } from '../../../../base/common/uri.js';
16
import { localize } from '../../../../nls.js';
17
import { agentHostUri } from '../../../../platform/agentHost/common/agentHostFileSystemProvider.js';
18
import { AGENT_HOST_SCHEME, agentHostAuthority, toAgentHostUri } from '../../../../platform/agentHost/common/agentHostUri.js';
19
import { AgentSession, type IAgentConnection, type IAgentSessionMetadata } from '../../../../platform/agentHost/common/agentService.js';
20
import type { ISessionGitState } from '../../../../platform/agentHost/common/state/sessionState.js';
21
import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
22
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
23
import { ILabelService } from '../../../../platform/label/common/label.js';
24
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
25
import { INotificationService } from '../../../../platform/notification/common/notification.js';
26
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
27
import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
28
import { IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js';
29
import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
30
import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';
31
import { AgentHostSessionAdapter, BaseAgentHostSessionsProvider } from '../../agentHost/browser/baseAgentHostSessionsProvider.js';
32
import { buildAgentHostSessionWorkspace, readBranchProtectionPatterns } from '../../../common/agentHostSessionWorkspace.js';
33
import { ISession, ISessionType, ISessionWorkspace, ISessionWorkspaceBrowseAction, SESSION_WORKSPACE_GROUP_REMOTE } from '../../../services/sessions/common/session.js';
34
import { remoteAgentHostSessionTypeId } from '../common/remoteAgentHostSessionType.js';
35
36
/** Storage key prefix for cached session summaries, per remote address. */
37
const CACHED_SESSIONS_STORAGE_PREFIX = 'remoteAgentHost.cachedSessions.';
38
39
/** Maximum number of cached session summaries persisted per host. */
40
const CACHED_SESSIONS_MAX_PER_HOST = 100;
41
42
/**
43
* Serialized shape of an {@link IAgentSessionMetadata} suitable for
44
* persisting via {@link IStorageService}. URIs are stored as strings
45
* and diffs are intentionally omitted (they are re-populated when the
46
* connection refreshes sessions).
47
*/
48
interface ISerializedSessionMetadata {
49
readonly session: string;
50
readonly startTime: number;
51
readonly modifiedTime: number;
52
readonly summary?: string;
53
readonly model?: IAgentSessionMetadata['model'];
54
readonly workingDirectory?: string;
55
readonly isRead?: boolean;
56
readonly isArchived?: boolean;
57
/** @deprecated Legacy name for `isArchived`. */
58
readonly isDone?: boolean;
59
readonly project?: { readonly uri: string; readonly displayName: string };
60
}
61
62
function serializeMetadata(meta: IAgentSessionMetadata): ISerializedSessionMetadata {
63
return {
64
session: meta.session.toString(),
65
startTime: meta.startTime,
66
modifiedTime: meta.modifiedTime,
67
summary: meta.summary,
68
model: meta.model,
69
workingDirectory: meta.workingDirectory?.toString(),
70
isRead: meta.isRead,
71
isArchived: meta.isArchived,
72
project: meta.project ? { uri: meta.project.uri.toString(), displayName: meta.project.displayName } : undefined,
73
};
74
}
75
76
function deserializeMetadata(raw: ISerializedSessionMetadata): IAgentSessionMetadata | undefined {
77
try {
78
return {
79
session: URI.parse(raw.session),
80
startTime: raw.startTime,
81
modifiedTime: raw.modifiedTime,
82
summary: raw.summary,
83
model: raw.model,
84
workingDirectory: raw.workingDirectory ? URI.parse(raw.workingDirectory) : undefined,
85
isRead: raw.isRead,
86
isArchived: raw.isArchived ?? raw.isDone,
87
project: raw.project ? { uri: URI.parse(raw.project.uri), displayName: raw.project.displayName } : undefined,
88
};
89
} catch {
90
return undefined;
91
}
92
}
93
94
function toLocalProjectUri(uri: URI, connectionAuthority: string): URI {
95
return uri.scheme === Schemas.file ? toAgentHostUri(uri, connectionAuthority) : uri;
96
}
97
98
export interface IRemoteAgentHostSessionsProviderConfig {
99
readonly address: string;
100
readonly name: string;
101
/** Optional hook to establish a connection on demand (e.g. tunnel relay). */
102
readonly connectOnDemand?: () => Promise<void>;
103
/** Optional hook to tear down the active connection on demand (e.g. tunnel relay). */
104
readonly disconnectOnDemand?: () => Promise<void>;
105
}
106
107
/**
108
* Sessions provider for a remote agent host connection. A thin subclass of
109
* {@link BaseAgentHostSessionsProvider} that adds the connection-lifecycle
110
* surface (`setConnection`/`clearConnection`), sticky authentication-pending
111
* tracking, the well-known session-type mapping, and a remote folder picker.
112
*
113
* **URI/ID scheme:**
114
* - **rawId** - unique session identifier (e.g. `abc123`), used as the cache key.
115
* - **resource** - `{resourceScheme}:///{rawId}`. The scheme is the unique
116
* per-connection id and routes the chat service to the correct
117
* {@link AgentHostSessionHandler}.
118
* - **sessionType** - the logical session type (e.g. `copilotcli` for copilot
119
* agents, or the per-connection id for other agents). Distinct from the
120
* resource scheme.
121
* - **sessionId** - `{providerId}:{resource}` - the provider-scoped ID used by
122
* {@link ISessionsProvider} methods.
123
* - Protocol operations (e.g. `disposeSession`) use the canonical agent
124
* session URI (`copilot:///abc123`), reconstructed via {@link AgentSession.uri}.
125
*/
126
export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvider {
127
128
readonly id: string;
129
readonly label: string;
130
readonly icon: ThemeIcon = Codicon.remote;
131
readonly remoteAddress: string;
132
readonly browseActions: readonly ISessionWorkspaceBrowseAction[];
133
134
private _outputChannelId: string | undefined;
135
get outputChannelId(): string | undefined { return this._outputChannelId; }
136
137
private readonly _connectionStatus = observableValue<RemoteAgentHostConnectionStatus>('connectionStatus', RemoteAgentHostConnectionStatus.Disconnected);
138
readonly connectionStatus: IObservable<RemoteAgentHostConnectionStatus> = this._connectionStatus;
139
140
/**
141
* `true` while we are still resolving and pushing tokens for the host's
142
* `protectedResources`. Defaults to `true` so that sessions surface as
143
* loading until the first authentication pass settles.
144
*/
145
private readonly _authenticationPending = observableValue('authenticationPending', true);
146
private _authenticationSettled = false;
147
148
private readonly _onDidDisconnect = this._register(new Emitter<void>());
149
protected override get onConnectionLost(): Event<void> { return this._onDidDisconnect.event; }
150
151
/**
152
* Overridable seam so tests can exercise both the web and non-web
153
* branches of the label/description gating without depending on the
154
* ambient {@link isWeb} constant (the browser test runner always
155
* reports `isWeb === true`).
156
*/
157
protected get isWebPlatform(): boolean { return isWeb; }
158
159
private _connection: IAgentConnection | undefined;
160
private _defaultDirectory: string | undefined;
161
private readonly _connectionListeners = this._register(new DisposableStore());
162
private readonly _connectionAuthority: string;
163
private readonly _connectOnDemand: (() => Promise<void>) | undefined;
164
private readonly _disconnectOnDemand: (() => Promise<void>) | undefined;
165
/** Storage key used for persisting {@link _sessionCache} snapshots. */
166
private readonly _storageKey: string;
167
/**
168
* Set when {@link _sessionCache} has changed since the last persist.
169
* The actual write happens on the next `onWillSaveState` signal from
170
* {@link IStorageService} so that bursts of notifications do not
171
* repeatedly re-serialize the whole cache.
172
*/
173
private _cacheDirty = false;
174
/**
175
* Snapshot of the source metadata for each adapter in {@link _sessionCache},
176
* keyed by raw session ID. Captured in {@link createAdapter} and re-used by
177
* {@link _persistCache} to serialize sessions without having to reconstruct
178
* every `IAgentSessionMetadata` field from observables.
179
*/
180
private readonly _metaByRawId = new Map<string, IAgentSessionMetadata>();
181
/**
182
* When `true`, the provider has been marked unreachable and sessions are
183
* hidden from {@link getSessions}, even though {@link _sessionCache} and
184
* persistent storage are retained. Cleared when a new connection is wired
185
* up in {@link setConnection}, at which point the cached entries are
186
* re-announced so the UI can repopulate.
187
*/
188
private _unpublished = false;
189
190
constructor(
191
config: IRemoteAgentHostSessionsProviderConfig,
192
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
193
@INotificationService private readonly _notificationService: INotificationService,
194
@IStorageService private readonly _storageService: IStorageService,
195
@IChatSessionsService chatSessionsService: IChatSessionsService,
196
@IChatService chatService: IChatService,
197
@IChatWidgetService chatWidgetService: IChatWidgetService,
198
@ILanguageModelsService languageModelsService: ILanguageModelsService,
199
@IRemoteAgentHostService private readonly _remoteAgentHostService: IRemoteAgentHostService,
200
@ILabelService private readonly _labelService: ILabelService,
201
@IConfigurationService private readonly _configurationService: IConfigurationService,
202
) {
203
super(chatSessionsService, chatService, chatWidgetService, languageModelsService);
204
205
this._connectionAuthority = agentHostAuthority(config.address);
206
this._connectOnDemand = config.connectOnDemand;
207
this._disconnectOnDemand = config.disconnectOnDemand;
208
const displayName = config.name || config.address;
209
210
this.id = `agenthost-${this._connectionAuthority}`;
211
this.label = displayName;
212
this.remoteAddress = config.address;
213
this._storageKey = `${CACHED_SESSIONS_STORAGE_PREFIX}${this._connectionAuthority}`;
214
215
this.browseActions = [{
216
label: localize('folders', "Folders"),
217
description: displayName,
218
group: SESSION_WORKSPACE_GROUP_REMOTE,
219
icon: Codicon.remote,
220
providerId: this.id,
221
run: () => this._browseForFolder(),
222
}];
223
224
this._loadCachedSessions();
225
226
this._register(this._onDidChangeSessions.event(e => {
227
if (this._unpublished) {
228
return;
229
}
230
if (e.added.length > 0 || e.removed.length > 0 || e.changed.length > 0) {
231
this._cacheDirty = true;
232
}
233
for (const removed of e.removed) {
234
const rawId = this._rawIdFromChatId(removed.sessionId);
235
if (rawId) {
236
this._metaByRawId.delete(rawId);
237
}
238
}
239
}));
240
241
this._register(this._storageService.onWillSaveState(() => {
242
if (this._cacheDirty) {
243
this._persistCache();
244
this._cacheDirty = false;
245
}
246
}));
247
}
248
249
// -- BaseAgentHostSessionsProvider hooks ---------------------------------
250
251
protected get connection(): IAgentConnection | undefined { return this._connection; }
252
253
protected get authenticationPending(): IObservable<boolean> { return this._authenticationPending; }
254
255
protected override createAdapter(meta: IAgentSessionMetadata): AgentHostSessionAdapter {
256
this._metaByRawId.set(AgentSession.id(meta.session), meta);
257
return super.createAdapter(meta);
258
}
259
260
protected _adapterOptions() {
261
const web = this.isWebPlatform;
262
return {
263
description: web ? undefined : new MarkdownString().appendText(this.label),
264
buildWorkspace: (project: IAgentSessionMetadata['project'], workingDirectory: URI | undefined, gitState: ISessionGitState | undefined) => {
265
const uriForDescription = project?.uri ?? workingDirectory;
266
const description = uriForDescription ? this._labelService.getUriLabel(dirname(uriForDescription), { relative: false }) : undefined;
267
const branchProtectionPatterns = readBranchProtectionPatterns(this._configurationService, workingDirectory ?? project?.uri);
268
return RemoteAgentHostSessionsProvider.buildWorkspace(project, workingDirectory, web ? undefined : this.label, gitState, description, branchProtectionPatterns);
269
},
270
};
271
}
272
273
protected resourceSchemeForProvider(provider: string): string {
274
return remoteAgentHostSessionTypeId(this._connectionAuthority, provider);
275
}
276
277
override getSessions(): ISession[] {
278
return this._unpublished ? [] : super.getSessions();
279
}
280
281
protected override mapWorkingDirectoryUri(uri: URI): URI {
282
return toAgentHostUri(uri, this._connectionAuthority);
283
}
284
285
protected override mapProjectUri(uri: URI): URI {
286
return toLocalProjectUri(uri, this._connectionAuthority);
287
}
288
289
protected override _diffUriMapper(): (uri: URI) => URI {
290
return uri => toAgentHostUri(uri, this._connectionAuthority);
291
}
292
293
protected override _validateBeforeCreate(_sessionType: ISessionType): void {
294
if (!this._connection) {
295
throw new Error(localize('notConnectedSession', "Cannot create session: not connected to remote agent host '{0}'.", this.label));
296
}
297
}
298
299
protected override _noAgentsErrorMessage(): string {
300
return localize('noAgents', "Remote agent host '{0}' has not advertised any agents yet.", this.label);
301
}
302
303
protected override _notConnectedSendErrorMessage(): string {
304
return localize('notConnectedSend', "Cannot send request: not connected to remote agent host '{0}'.", this.label);
305
}
306
307
// -- Connection lifecycle ------------------------------------------------
308
309
/**
310
* Establish (or re-establish) the connection for this host on demand.
311
* Tunnel-backed providers use their relay hook; other providers fall
312
* back to the generic remote agent host reconnect path.
313
*/
314
async connect(): Promise<void> {
315
if (this._connectOnDemand) {
316
await this._connectOnDemand();
317
return;
318
}
319
this._remoteAgentHostService.reconnect(this.remoteAddress);
320
}
321
322
/**
323
* Tear down the active connection for this host. Tunnel-backed providers
324
* use their relay hook; other providers fall back to the generic remote
325
* agent host disconnect path. Cached sessions are hidden from the UI so
326
* the sessions list reflects the disconnected state; the persisted cache
327
* is retained so sessions can be restored on reconnect.
328
*/
329
async disconnect(): Promise<void> {
330
this.unpublishCachedSessions();
331
if (this._disconnectOnDemand) {
332
await this._disconnectOnDemand();
333
return;
334
}
335
await this._remoteAgentHostService.removeRemoteAgentHost(this.remoteAddress);
336
}
337
338
/** Update the connection status for this provider. */
339
setConnectionStatus(status: RemoteAgentHostConnectionStatus): void {
340
this._connectionStatus.set(status, undefined);
341
}
342
343
/** Set the output channel ID for this provider's IPC log. */
344
setOutputChannelId(id: string): void {
345
this._outputChannelId = id;
346
}
347
348
setAuthenticationPending(pending: boolean): void {
349
// Sticky: once the first authentication pass settles, never surface
350
// pending again. Subsequent re-auths happen silently in the background.
351
if (this._authenticationSettled) {
352
return;
353
}
354
if (!pending) {
355
this._authenticationSettled = true;
356
}
357
this._authenticationPending.set(pending, undefined);
358
}
359
360
/**
361
* Wire a live connection to this provider, enabling session operations and folder browsing.
362
*/
363
setConnection(connection: IAgentConnection, defaultDirectory?: string): void {
364
if (this._connection === connection && this._defaultDirectory === defaultDirectory) {
365
return;
366
}
367
368
this._connectionListeners.clear();
369
this._sessionStateSubscriptions.clearAndDisposeAll();
370
this._connection = connection;
371
this._defaultDirectory = defaultDirectory;
372
this._unpublished = false;
373
374
// Dynamically discover session types from the host's advertised agents.
375
const rootStateValue = connection.rootState.value;
376
if (rootStateValue && !(rootStateValue instanceof Error)) {
377
this._syncSessionTypesFromRootState(rootStateValue);
378
this._syncRootConfigFromRootState(rootStateValue);
379
}
380
this._connectionListeners.add(connection.rootState.onDidChange(rootState => {
381
this._syncSessionTypesFromRootState(rootState);
382
this._syncRootConfigFromRootState(rootState);
383
}));
384
385
this._attachConnectionListeners(connection, this._connectionListeners);
386
387
// Always refresh sessions when a connection is (re)established
388
this._cacheInitialized = true;
389
this._refreshSessions();
390
}
391
392
/**
393
* Clear the connection, e.g. when the remote host disconnects.
394
* Retains the provider registration so it remains visible in the UI,
395
* and **preserves** the cached session list so previously loaded
396
* sessions stay visible while we're offline. Callers that know the
397
* host is unreachable should follow up with {@link unpublishCachedSessions}.
398
*/
399
clearConnection(): void {
400
this._connectionListeners.clear();
401
this._sessionStateSubscriptions.clearAndDisposeAll();
402
this._onDidDisconnect.fire();
403
this._connection = undefined;
404
this._defaultDirectory = undefined;
405
if (this._currentNewSession) {
406
this._clearNewSessionConfig(this._currentNewSession.sessionId);
407
this._currentNewSession = undefined;
408
}
409
this._currentNewSessionStatus = undefined;
410
this._currentNewSessionModelId = undefined;
411
this._currentNewSessionLoading = undefined;
412
this._selectedModelId = undefined;
413
414
if (this._sessionTypes.length > 0) {
415
this._sessionTypes = [];
416
this._onDidChangeSessionTypes.fire();
417
}
418
419
// Drop only the transient pending/draft session; keep the persisted
420
// cache so the workspace picker keeps showing offline sessions.
421
if (this._pendingSession) {
422
const pending = this._pendingSession;
423
this._pendingSession = undefined;
424
this._onDidChangeSessions.fire({ added: [], removed: [pending], changed: [] });
425
}
426
427
// Reset the in-memory cache-initialized flag so a fresh connection
428
// triggers a full list refresh (which will reconcile against the
429
// persisted entries we keep on disk).
430
this._cacheInitialized = false;
431
}
432
433
/**
434
* Hide cached sessions from the UI without discarding them. Called by the
435
* host-tracking contributions when they determine the remote host is
436
* unreachable (tunnel offline or SSH reconnect failed). The in-memory
437
* cache and persisted storage are left intact so the sessions can be
438
* restored if the host comes back online in this session, or on the next
439
* launch. The next {@link setConnection} call re-announces the cached
440
* entries.
441
*/
442
unpublishCachedSessions(): void {
443
if (this._unpublished) {
444
return;
445
}
446
this._unpublished = true;
447
const removed: ISession[] = Array.from(this._sessionCache.values());
448
if (removed.length > 0) {
449
this._onDidChangeSessions.fire({ added: [], removed, changed: [] });
450
}
451
}
452
453
/** Load persisted session summaries into {@link _sessionCache}. */
454
private _loadCachedSessions(): void {
455
const parsed = this._storageService.getObject(this._storageKey, StorageScope.APPLICATION);
456
if (!Array.isArray(parsed)) {
457
return;
458
}
459
for (const entry of parsed as readonly ISerializedSessionMetadata[]) {
460
const meta = deserializeMetadata(entry);
461
if (!meta) {
462
continue;
463
}
464
const rawId = AgentSession.id(meta.session);
465
if (this._sessionCache.has(rawId)) {
466
continue;
467
}
468
const cached = this.createAdapter(meta);
469
this._sessionCache.set(rawId, cached);
470
}
471
}
472
473
/**
474
* Persist the current {@link _sessionCache} to storage, capping at
475
* {@link CACHED_SESSIONS_MAX_PER_HOST} most-recently-modified entries.
476
* Mutable fields are read from each adapter's observables and overlaid on
477
* top of the original metadata snapshot captured in {@link _metaByRawId}.
478
*/
479
private _persistCache(): void {
480
const entries: ISerializedSessionMetadata[] = [];
481
for (const [rawId, adapter] of this._sessionCache) {
482
const base = this._metaByRawId.get(rawId);
483
if (!base) {
484
continue;
485
}
486
entries.push(serializeMetadata({
487
...base,
488
summary: adapter.title.get() || base.summary,
489
modifiedTime: adapter.updatedAt.get().getTime(),
490
model: adapter.modelSelection ?? base.model,
491
isRead: adapter.isRead.get(),
492
isArchived: adapter.isArchived.get(),
493
}));
494
}
495
if (entries.length === 0) {
496
this._storageService.remove(this._storageKey, StorageScope.APPLICATION);
497
return;
498
}
499
entries.sort((a, b) => b.modifiedTime - a.modifiedTime);
500
const limited = entries.slice(0, CACHED_SESSIONS_MAX_PER_HOST);
501
this._storageService.store(this._storageKey, JSON.stringify(limited), StorageScope.APPLICATION, StorageTarget.USER);
502
}
503
504
// -- Session-type sync ---------------------------------------------------
505
506
protected _formatSessionTypeLabel(agentLabel: string): string {
507
return `${agentLabel} [${this.label}]`;
508
}
509
510
// -- Workspaces ----------------------------------------------------------
511
512
static buildWorkspace(project: IAgentSessionMetadata['project'], workingDirectory: URI | undefined, providerLabel: string | undefined, gitState: ISessionGitState | undefined, description?: string, branchProtectionPatterns?: readonly string[]): ISessionWorkspace | undefined {
513
return buildAgentHostSessionWorkspace(project, workingDirectory, { providerLabel, fallbackIcon: Codicon.remote, requiresWorkspaceTrust: false, description, branchProtectionPatterns, group: SESSION_WORKSPACE_GROUP_REMOTE }, gitState);
514
}
515
516
private _buildWorkspaceFromUri(uri: URI): ISessionWorkspace {
517
const folderName = basename(uri) || uri.path;
518
return {
519
label: this.isWebPlatform ? folderName : `${folderName} [${this.label}]`,
520
description: this._labelService.getUriLabel(dirname(uri), { relative: false }),
521
group: SESSION_WORKSPACE_GROUP_REMOTE,
522
icon: Codicon.remote,
523
repositories: [{ uri, workingDirectory: undefined, detail: undefined, baseBranchName: undefined }],
524
requiresWorkspaceTrust: true,
525
};
526
}
527
528
resolveWorkspace(repositoryUri: URI): ISessionWorkspace | undefined {
529
if (repositoryUri.scheme !== AGENT_HOST_SCHEME) {
530
return undefined;
531
}
532
return this._buildWorkspaceFromUri(repositoryUri);
533
}
534
535
// -- Browse --------------------------------------------------------------
536
537
private async _browseForFolder(): Promise<ISessionWorkspace | undefined> {
538
// Establish connection on demand if a hook is provided (e.g. tunnel relay)
539
if (!this._connection && this._connectOnDemand) {
540
try {
541
await this._connectOnDemand();
542
} catch (err) {
543
this._notificationService.error(localize('connectFailed', "Failed to connect to remote agent host '{0}': {1}", this.label, err instanceof Error ? err.message : String(err)));
544
return undefined;
545
}
546
}
547
548
if (!this._connection) {
549
this._notificationService.error(localize('notConnected', "Unable to connect to remote agent host '{0}'.", this.label));
550
return undefined;
551
}
552
553
const defaultUri = agentHostUri(this._connectionAuthority, this._defaultDirectory ?? '/');
554
555
try {
556
const selected = await this._fileDialogService.showOpenDialog({
557
canSelectFiles: false,
558
canSelectFolders: true,
559
canSelectMany: false,
560
title: localize('selectRemoteFolder', "Select Folder on {0}", this.label),
561
availableFileSystems: [AGENT_HOST_SCHEME],
562
defaultUri,
563
});
564
if (selected?.[0]) {
565
return this._buildWorkspaceFromUri(selected[0]);
566
}
567
} catch {
568
// dialog was cancelled or failed
569
}
570
return undefined;
571
}
572
}
573
574