Skip to content

Commit 4fd4618

Browse files
authored
Merge pull request #311472 from microsoft/connor4312/ah-auto-connect
agentHost: auto-connect for tunnels and cached sessions
1 parent 7a0f366 commit 4fd4618

File tree

7 files changed

+353
-39
lines changed

7 files changed

+353
-39
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export const RemoteAgentHostsSettingId = 'chat.remoteAgentHosts';
2222
/** Configuration key to enable remote agent host connections. */
2323
export const RemoteAgentHostsEnabledSettingId = 'chat.remoteAgentHostsEnabled';
2424

25+
/**
26+
* Configuration key that controls whether online dev tunnels and
27+
* configured SSH remote agent hosts are auto-connected at startup.
28+
*/
29+
export const RemoteAgentHostAutoConnectSettingId = 'chat.remoteAgentHostsAutoConnect';
30+
2531
export const enum RemoteAgentHostEntryType {
2632
WebSocket = 'websocket',
2733
SSH = 'ssh',

src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { URI } from '../../../../base/common/uri.js';
1414
import { generateUuid } from '../../../../base/common/uuid.js';
1515
import { localize } from '../../../../nls.js';
1616
import { AgentSession, IAgentConnection, IAgentSessionMetadata } from '../../../../platform/agentHost/common/agentService.js';
17-
import { NotificationType } from '../../../../platform/agentHost/common/state/protocol/notifications.js';
1817
import { IResolveSessionConfigResult } from '../../../../platform/agentHost/common/state/protocol/commands.js';
18+
import { NotificationType } from '../../../../platform/agentHost/common/state/protocol/notifications.js';
1919
import type { IFileEdit, IModelSelection, ISessionConfigPropertySchema, ISessionState, ISessionSummary } from '../../../../platform/agentHost/common/state/protocol/state.js';
2020
import { ActionType, isSessionAction } from '../../../../platform/agentHost/common/state/sessionActions.js';
2121
import { StateComponents } from '../../../../platform/agentHost/common/state/sessionState.js';
@@ -24,12 +24,12 @@ import { IChatSendRequestOptions, IChatService } from '../../../../workbench/con
2424
import { IChatSessionFileChange, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
2525
import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js';
2626
import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';
27-
import { agentHostSessionWorkspaceKey } from '../../../common/agentHostSessionWorkspace.js';
28-
import { diffsToChanges, diffsEqual, mapProtocolStatus } from '../../../common/agentHostDiffs.js';
27+
import { diffsEqual, diffsToChanges, mapProtocolStatus } from '../../../common/agentHostDiffs.js';
2928
import { buildMutableConfigSchema, IAgentHostSessionsProvider, resolvedConfigsEqual } from '../../../common/agentHostSessionsProvider.js';
29+
import { agentHostSessionWorkspaceKey } from '../../../common/agentHostSessionWorkspace.js';
3030
import { isSessionConfigComplete } from '../../../common/sessionConfig.js';
31-
import { ISendRequestOptions, ISessionChangeEvent } from '../../../services/sessions/common/sessionsProvider.js';
3231
import { IChat, IGitHubInfo, ISession, ISessionType, ISessionWorkspace, ISessionWorkspaceBrowseAction, SessionStatus } from '../../../services/sessions/common/session.js';
32+
import { ISendRequestOptions, ISessionChangeEvent } from '../../../services/sessions/common/sessionsProvider.js';
3333

3434
// ============================================================================
3535
// AgentHostSessionAdapter — shared adapter for local and remote sessions

src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,10 @@ export class WorkspacePicker extends Disposable {
197197
return;
198198
}
199199
if (item.remoteProvider && item.browseActionIndex === undefined) {
200-
if (item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) {
201-
// Disconnected tunnel — trigger connection flow
202-
this.commandService.executeCommand('workbench.action.sessions.connectViaTunnel');
203-
} else {
204-
// Disconnected SSH host — show options menu after widget hides
200+
if (!item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) {
201+
// Disconnected SSH host — show options menu after widget hides.
202+
// (Disconnected tunnels are rendered as disabled with a
203+
// refresh toolbar action, so onSelect doesn't fire for them.)
205204
this._showRemoteHostOptionsDelayed(item.remoteProvider);
206205
}
207206
} else if (item.browseActionIndex !== undefined) {
@@ -437,11 +436,27 @@ export class WorkspacePicker extends Disposable {
437436
const status = provider.connectionStatus!.get();
438437
const isConnected = status === RemoteAgentHostConnectionStatus.Connected;
439438
const providerBrowseIndex = allBrowseActions.findIndex(a => a.providerId === provider.id);
439+
const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX);
440440

441441
const toolbarActions: IAction[] = [];
442442

443-
// Gear menu only for SSH hosts, not tunnel providers
444-
if (!provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) {
443+
if (isTunnel) {
444+
// Offline/connecting tunnels: surface a refresh button that
445+
// attempts to (re)connect in case the cached status is stale.
446+
if (!isConnected && providerBrowseIndex >= 0) {
447+
const browseIndex = providerBrowseIndex;
448+
toolbarActions.push(toAction({
449+
id: `workspacePicker.remote.refresh.${provider.id}`,
450+
label: localize('workspacePicker.refreshTunnel', "Attempt to Connect"),
451+
class: ThemeIcon.asClassName(Codicon.refresh),
452+
run: () => {
453+
this.actionWidgetService.hide();
454+
this._executeBrowseAction(browseIndex);
455+
},
456+
}));
457+
}
458+
} else {
459+
// Gear menu only for SSH hosts, not tunnel providers
445460
toolbarActions.push(toAction({
446461
id: `workspacePicker.remote.gear.${provider.id}`,
447462
label: localize('workspacePicker.remoteOptions', "Options"),
@@ -453,15 +468,13 @@ export class WorkspacePicker extends Disposable {
453468
}));
454469
}
455470

456-
const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX);
457-
458471
items.push({
459472
kind: ActionListItemKind.Action,
460473
label: provider.label,
461474
description: this._getStatusDescription(status),
462475
hover: { content: this._getStatusHover(status, provider.remoteAddress) },
463476
group: { title: '', icon: isTunnel ? Codicon.cloud : Codicon.remote },
464-
disabled: isTunnel ? false : !isConnected,
477+
disabled: !isConnected,
465478
item: {
466479
browseActionIndex: isConnected && providerBrowseIndex >= 0 ? providerBrowseIndex : undefined,
467480
remoteProvider: provider,

src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js';
1010
import * as nls from '../../../../nls.js';
1111
import { agentHostAuthority } from '../../../../platform/agentHost/common/agentHostUri.js';
1212
import { type AgentProvider, type IAgentConnection } from '../../../../platform/agentHost/common/agentService.js';
13-
import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
13+
import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostAutoConnectSettingId, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
1414
import { TunnelAgentHostsSettingId } from '../../../../platform/agentHost/common/tunnelAgentHost.js';
1515
import { type IProtectedResourceMetadata, type URI as ProtocolURI } from '../../../../platform/agentHost/common/state/protocol/state.js';
1616
import { type IAgentInfo, type ICustomizationRef, type IRootState } from '../../../../platform/agentHost/common/state/sessionState.js';
@@ -102,7 +102,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
102102

103103
// Reconcile providers when configured entries change
104104
this._register(this._configurationService.onDidChangeConfiguration(e => {
105-
if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId)) {
105+
if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId) || e.affectsConfiguration(RemoteAgentHostAutoConnectSettingId)) {
106106
this._reconcile();
107107
}
108108
}));
@@ -194,6 +194,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
194194
* sshConfigHost but no active connection.
195195
*/
196196
private _reconnectSSHEntries(): void {
197+
const autoConnect = this._configurationService.getValue<boolean>(RemoteAgentHostAutoConnectSettingId);
197198
const entries = this._remoteAgentHostService.configuredEntries;
198199
for (const entry of entries) {
199200
if (entry.connection.type !== RemoteAgentHostEntryType.SSH || !entry.connection.sshConfigHost) {
@@ -208,6 +209,9 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
208209
if (hasConnection || this._pendingSSHReconnects.has(sshConfigHost)) {
209210
continue;
210211
}
212+
if (!autoConnect) {
213+
continue;
214+
}
211215
this._pendingSSHReconnects.add(sshConfigHost);
212216
this._logService.info(`[RemoteAgentHost] Re-establishing SSH tunnel for ${sshConfigHost}`);
213217
this._sshService.reconnect(sshConfigHost, entry.name).then(() => {
@@ -216,6 +220,10 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
216220
}).catch(err => {
217221
this._pendingSSHReconnects.delete(sshConfigHost);
218222
this._logService.error(`[RemoteAgentHost] SSH reconnect failed for ${sshConfigHost}`, err);
223+
// Host is unreachable — unpublish any cached sessions we
224+
// were showing so the UI doesn't list stale entries for a
225+
// host we cannot currently reach.
226+
this._providerInstances.get(address)?.unpublishCachedSessions();
219227
});
220228
}
221229
}
@@ -590,6 +598,13 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
590598
scope: ConfigurationScope.APPLICATION,
591599
tags: ['experimental', 'advanced'],
592600
},
601+
[RemoteAgentHostAutoConnectSettingId]: {
602+
type: 'boolean',
603+
description: nls.localize('chat.remoteAgentHosts.autoConnect', "Automatically connect to online dev tunnel and SSH-configured remote agent hosts on startup. When disabled, cached sessions are still shown but connections are established only on demand."),
604+
default: true,
605+
scope: ConfigurationScope.APPLICATION,
606+
tags: ['experimental', 'advanced'],
607+
},
593608
'chat.sshRemoteAgentHostCommand': {
594609
type: 'string',
595610
description: nls.localize('chat.sshRemoteAgentHostCommand', "For development: Override the command used to start the remote agent host over SSH. When set, skips automatic CLI installation and runs this command instead. The command must print a WebSocket URL matching ws://127.0.0.1:PORT (optionally with ?tkn=TOKEN) to stdout or stderr./"),

0 commit comments

Comments
 (0)