Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/vs/platform/agentHost/common/remoteAgentHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const RemoteAgentHostsSettingId = 'chat.remoteAgentHosts';
/** Configuration key to enable remote agent host connections. */
export const RemoteAgentHostsEnabledSettingId = 'chat.remoteAgentHostsEnabled';

/**
* Configuration key that controls whether online dev tunnels and
* configured SSH remote agent hosts are auto-connected at startup.
*/
export const RemoteAgentHostAutoConnectSettingId = 'chat.remoteAgentHostsAutoConnect';

export const enum RemoteAgentHostEntryType {
WebSocket = 'websocket',
SSH = 'ssh',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { URI } from '../../../../base/common/uri.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { localize } from '../../../../nls.js';
import { AgentSession, IAgentConnection, IAgentSessionMetadata } from '../../../../platform/agentHost/common/agentService.js';
import { NotificationType } from '../../../../platform/agentHost/common/state/protocol/notifications.js';
import { IResolveSessionConfigResult } from '../../../../platform/agentHost/common/state/protocol/commands.js';
import { NotificationType } from '../../../../platform/agentHost/common/state/protocol/notifications.js';
import type { IFileEdit, IModelSelection, ISessionConfigPropertySchema, ISessionState, ISessionSummary } from '../../../../platform/agentHost/common/state/protocol/state.js';
import { ActionType, isSessionAction } from '../../../../platform/agentHost/common/state/sessionActions.js';
import { StateComponents } from '../../../../platform/agentHost/common/state/sessionState.js';
Expand All @@ -24,12 +24,12 @@ import { IChatSendRequestOptions, IChatService } from '../../../../workbench/con
import { IChatSessionFileChange, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js';
import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';
import { agentHostSessionWorkspaceKey } from '../../../common/agentHostSessionWorkspace.js';
import { diffsToChanges, diffsEqual, mapProtocolStatus } from '../../../common/agentHostDiffs.js';
import { diffsEqual, diffsToChanges, mapProtocolStatus } from '../../../common/agentHostDiffs.js';
import { buildMutableConfigSchema, IAgentHostSessionsProvider, resolvedConfigsEqual } from '../../../common/agentHostSessionsProvider.js';
import { agentHostSessionWorkspaceKey } from '../../../common/agentHostSessionWorkspace.js';
import { isSessionConfigComplete } from '../../../common/sessionConfig.js';
import { ISendRequestOptions, ISessionChangeEvent } from '../../../services/sessions/common/sessionsProvider.js';
import { IChat, IGitHubInfo, ISession, ISessionType, ISessionWorkspace, ISessionWorkspaceBrowseAction, SessionStatus } from '../../../services/sessions/common/session.js';
import { ISendRequestOptions, ISessionChangeEvent } from '../../../services/sessions/common/sessionsProvider.js';

// ============================================================================
// AgentHostSessionAdapter — shared adapter for local and remote sessions
Expand Down
33 changes: 23 additions & 10 deletions src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,10 @@ export class WorkspacePicker extends Disposable {
return;
}
if (item.remoteProvider && item.browseActionIndex === undefined) {
if (item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) {
// Disconnected tunnel — trigger connection flow
this.commandService.executeCommand('workbench.action.sessions.connectViaTunnel');
} else {
// Disconnected SSH host — show options menu after widget hides
if (!item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) {
// Disconnected SSH host — show options menu after widget hides.
// (Disconnected tunnels are rendered as disabled with a
// refresh toolbar action, so onSelect doesn't fire for them.)
this._showRemoteHostOptionsDelayed(item.remoteProvider);
}
} else if (item.browseActionIndex !== undefined) {
Expand Down Expand Up @@ -437,11 +436,27 @@ export class WorkspacePicker extends Disposable {
const status = provider.connectionStatus!.get();
const isConnected = status === RemoteAgentHostConnectionStatus.Connected;
const providerBrowseIndex = allBrowseActions.findIndex(a => a.providerId === provider.id);
const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX);

const toolbarActions: IAction[] = [];

// Gear menu only for SSH hosts, not tunnel providers
if (!provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) {
if (isTunnel) {
// Offline/connecting tunnels: surface a refresh button that
// attempts to (re)connect in case the cached status is stale.
if (!isConnected && providerBrowseIndex >= 0) {
const browseIndex = providerBrowseIndex;
toolbarActions.push(toAction({
id: `workspacePicker.remote.refresh.${provider.id}`,
label: localize('workspacePicker.refreshTunnel', "Attempt to Connect"),
class: ThemeIcon.asClassName(Codicon.refresh),
run: () => {
this.actionWidgetService.hide();
this._executeBrowseAction(browseIndex);
},
}));
}
Comment thread
connor4312 marked this conversation as resolved.
} else {
// Gear menu only for SSH hosts, not tunnel providers
toolbarActions.push(toAction({
id: `workspacePicker.remote.gear.${provider.id}`,
label: localize('workspacePicker.remoteOptions', "Options"),
Expand All @@ -453,15 +468,13 @@ export class WorkspacePicker extends Disposable {
}));
}

const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX);

items.push({
kind: ActionListItemKind.Action,
label: provider.label,
description: this._getStatusDescription(status),
hover: { content: this._getStatusHover(status, provider.remoteAddress) },
group: { title: '', icon: isTunnel ? Codicon.cloud : Codicon.remote },
disabled: isTunnel ? false : !isConnected,
disabled: !isConnected,
item: {
browseActionIndex: isConnected && providerBrowseIndex >= 0 ? providerBrowseIndex : undefined,
remoteProvider: provider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js';
import * as nls from '../../../../nls.js';
import { agentHostAuthority } from '../../../../platform/agentHost/common/agentHostUri.js';
import { type AgentProvider, type IAgentConnection } from '../../../../platform/agentHost/common/agentService.js';
import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostAutoConnectSettingId, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
import { TunnelAgentHostsSettingId } from '../../../../platform/agentHost/common/tunnelAgentHost.js';
import { type IProtectedResourceMetadata, type URI as ProtocolURI } from '../../../../platform/agentHost/common/state/protocol/state.js';
import { type IAgentInfo, type ICustomizationRef, type IRootState } from '../../../../platform/agentHost/common/state/sessionState.js';
Expand Down Expand Up @@ -102,7 +102,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc

// Reconcile providers when configured entries change
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId)) {
if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId) || e.affectsConfiguration(RemoteAgentHostAutoConnectSettingId)) {
this._reconcile();
}
}));
Expand Down Expand Up @@ -194,6 +194,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
* sshConfigHost but no active connection.
*/
private _reconnectSSHEntries(): void {
const autoConnect = this._configurationService.getValue<boolean>(RemoteAgentHostAutoConnectSettingId);
const entries = this._remoteAgentHostService.configuredEntries;
for (const entry of entries) {
if (entry.connection.type !== RemoteAgentHostEntryType.SSH || !entry.connection.sshConfigHost) {
Expand All @@ -208,6 +209,9 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
if (hasConnection || this._pendingSSHReconnects.has(sshConfigHost)) {
continue;
}
if (!autoConnect) {
continue;
}
this._pendingSSHReconnects.add(sshConfigHost);
this._logService.info(`[RemoteAgentHost] Re-establishing SSH tunnel for ${sshConfigHost}`);
this._sshService.reconnect(sshConfigHost, entry.name).then(() => {
Expand All @@ -216,6 +220,10 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
}).catch(err => {
this._pendingSSHReconnects.delete(sshConfigHost);
this._logService.error(`[RemoteAgentHost] SSH reconnect failed for ${sshConfigHost}`, err);
// Host is unreachable — unpublish any cached sessions we
// were showing so the UI doesn't list stale entries for a
// host we cannot currently reach.
this._providerInstances.get(address)?.unpublishCachedSessions();
});
}
}
Expand Down Expand Up @@ -590,6 +598,13 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
scope: ConfigurationScope.APPLICATION,
tags: ['experimental', 'advanced'],
},
[RemoteAgentHostAutoConnectSettingId]: {
type: 'boolean',
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."),
default: true,
scope: ConfigurationScope.APPLICATION,
tags: ['experimental', 'advanced'],
},
'chat.sshRemoteAgentHostCommand': {
type: 'string',
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./"),
Expand Down
Loading
Loading