Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion src/vs/platform/agentHost/common/wslRemoteAgentHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { IRelayMessage } from './relayTransport.js';
*/
export const WSL_REMOTE_AGENT_HOST_CHANNEL = 'wslRemoteAgentHost';

export const WSL_INSTALL_DOCS_URL = 'https://aka.ms/vscode-remote/wsl/install-wsl';
export const WSL_INSTALL_DOCS_URL = 'https://aka.ms/vscode-agent-wsl';

/**
* Prefix for WSL display addresses (`wsl:<distro>`). This is NOT a `ws://`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { Menus } from '../../../../browser/menus.js';
import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js';
import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js';
import { IAgentHostSessionsProvider, isAgentHostProvider } from '../../../../common/agentHostSessionsProvider.js';
import { runServerUpgrade } from './remoteHostOptions.js';
import { confirmTunnelDeletion, runServerUpgrade } from './remoteHostOptions.js';
import { IConnectionDiagnosticsService } from './connectionDiagnostics.js';
import { SESSION_WORKSPACE_GROUP_REMOTE } from '../../../../services/sessions/common/session.js';
import { ISessionsPartService } from '../../../../services/sessions/browser/sessionsPartService.js';
Expand Down Expand Up @@ -1019,13 +1019,7 @@ async function promptToConnectViaTunnel(
tunnelPicker.ignoreFocusOut = true;
let keepOpen = true;
try {
const confirmation = await dialogService.confirm({
type: 'warning',
message: localize('tunnelDeleteConfirmation', "Are you sure you want to delete dev tunnel '{0}'?", event.item.tunnel.name),
detail: localize('tunnelDeleteDetail', "The tunnel may be recreated if a machine starts hosting it again."),
primaryButton: localize('tunnelDeleteButton', "&&Delete"),
});
if (!confirmation.confirmed) {
if (!await confirmTunnelDeletion(dialogService, event.item.tunnel.name)) {
return;
}
if (isHostedTunnel(event.item.tunnel)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ export async function removeRemoteHost(provider: IAgentHostSessionsProvider, rem
}
}

export async function confirmTunnelDeletion(dialogService: IDialogService, name: string): Promise<boolean> {
const { confirmed } = await dialogService.confirm({
type: 'warning',
message: localize('tunnelDeleteConfirmation', "Are you sure you want to delete dev tunnel '{0}'?", name),
detail: localize('tunnelDeleteDetail', "This deletes the tunnel registration and disconnects its clients. Files on the remote machine will not be deleted. The tunnel may be recreated if a machine starts hosting it again."),
primaryButton: localize('tunnelDeleteButton', "&&Delete"),
});
return confirmed;
}

export async function renameRemoteHost(
provider: Pick<IAgentHostSessionsProvider, 'remoteAddress' | 'label' | 'setDisplayName'>,
quickInputService: IQuickInputService,
Expand Down Expand Up @@ -101,19 +111,17 @@ export function hasUpgradeReconnectStarted(status: RemoteAgentHostConnectionStat
* surfaced error). All user-facing errors are reported via the
* notification service.
*/
export async function runServerUpgrade(
accessor: ServicesAccessor,
export async function upgradeRemoteHost(
provider: IAgentHostSessionsProvider,
upgradeMethod: string,
remoteAgentHostService: IRemoteAgentHostService,
notificationService: INotificationService,
progressService: IProgressService,
): Promise<void> {
const address = provider.remoteAddress;
if (!address) {
return;
}
const remoteAgentHostService = accessor.get(IRemoteAgentHostService);
const notificationService = accessor.get(INotificationService);
const progressService = accessor.get(IProgressService);

await progressService.withProgress(
{
location: ProgressLocation.Notification,
Expand Down Expand Up @@ -194,6 +202,10 @@ export async function runServerUpgrade(
);
}

export async function runServerUpgrade(accessor: ServicesAccessor, provider: IAgentHostSessionsProvider, upgradeMethod: string): Promise<void> {
await upgradeRemoteHost(provider, upgradeMethod, accessor.get(IRemoteAgentHostService), accessor.get(INotificationService), accessor.get(IProgressService));
}

/**
* Surface a transient notification each time a provider transitions into
* the `incompatible` state. Fires once per transition (not on every
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface IRemoteTableRow {
}

type RemoteColumn = 'gutter' | 'name' | 'connection' | 'actions';
export type RemoteRowAction = 'toggleGroup' | 'toggleVisibility' | 'configure' | 'documentation' | 'connect' | 'rename' | 'delete' | 'signIn';
export type RemoteRowAction = 'toggleGroup' | 'toggleVisibility' | 'configure' | 'documentation' | 'connect' | 'rename' | 'copyAddress' | 'locationPreference' | 'updateServer' | 'delete' | 'signIn';

type RemoteCellTemplate = {
readonly container: HTMLElement;
Expand All @@ -72,7 +72,7 @@ type RemoteCellTemplate = {
| { readonly column: 'name'; readonly label: HTMLElement; readonly count: HTMLElement; readonly progress: HTMLElement; readonly warning: HTMLElement; readonly description: HTMLElement; readonly links: HTMLElement; readonly actionContainer: HTMLElement; actionLink?: Link; actionProgress?: HTMLElement; link?: Link }
| { readonly column: 'connection'; readonly label: HTMLElement; readonly warning: HTMLElement }
| { readonly column: 'gutter'; readonly action: Action; readonly toolbar: WorkbenchToolBar }
| { readonly column: 'actions'; readonly action: Action; readonly rename: Action; readonly delete: Action; readonly documentation: Action; readonly toolbar: WorkbenchToolBar; actionLayout: string }
| { readonly column: 'actions'; readonly action: Action; readonly rename: Action; readonly copyAddress: Action; readonly locationPreference: Action; readonly updateServer: Action; readonly delete: Action; readonly documentation: Action; readonly toolbar: WorkbenchToolBar; actionLayout: string }
);

export function getConnectionLabel(remote: ICustomizationRemote, connecting?: boolean): string {
Expand Down Expand Up @@ -158,7 +158,22 @@ export class RemotesTableRenderer implements ITableRenderer<IRemoteTableRow, Rem
this.onAction(template.row.entry, 'delete');
}
}));
template = { ...base, column: this.column, action, rename, delete: deleteAction, documentation, toolbar, actionLayout: '' };
const copyAddress = disposables.add(new Action('remotes.copyAddress', '', ThemeIcon.asClassName(Codicon.copy), true, async () => {
if (template.row?.entry.type === 'remote') {
this.onAction(template.row.entry, 'copyAddress');
}
}));
const locationPreference = disposables.add(new Action('remotes.locationPreference', '', ThemeIcon.asClassName(Codicon.serverProcess), true, async () => {
if (template.row?.entry.type === 'remote') {
this.onAction(template.row.entry, 'locationPreference');
}
}));
const updateServer = disposables.add(new Action('remotes.updateServer', '', ThemeIcon.asClassName(Codicon.cloudDownload), true, async () => {
if (template.row?.entry.type === 'remote') {
this.onAction(template.row.entry, 'updateServer');
}
}));
template = { ...base, column: this.column, action, rename, copyAddress, locationPreference, updateServer, delete: deleteAction, documentation, toolbar, actionLayout: '' };
}
disposables.add(dom.addDisposableGenericMouseDownListener(container, event => dom.EventHelper.stop(event, true)));
break;
Expand Down Expand Up @@ -279,11 +294,18 @@ export class RemotesTableRenderer implements ITableRenderer<IRemoteTableRow, Rem
const showConfig = template.column === 'actions' && entry.type === 'group' && (entry.kind === 'ssh' || entry.kind === 'tunnel' || entry.kind === 'environment');
const showDocumentation = template.column === 'actions' && entry.type === 'group' && !!entry.documentation;
const showConnection = template.column === 'actions' && entry.type === 'remote';
const showDelete = showConnection && entry.remote.kind === 'environment' && entry.remote.canRemove;
const actionLayout = `${showConfig}:${showDocumentation}:${showConnection}:${showDelete}`;
const showDelete = showConnection && (entry.remote.kind === 'environment' && entry.remote.canRemove || entry.remote.kind === 'tunnel' && !!entry.remote.canDelete);
const showLocationPreference = showConnection && (entry.remote.kind === 'tunnel' || entry.remote.kind === 'ssh');
const showUpdate = showConnection && RemoteAgentHostConnectionStatus.isIncompatible(entry.remote.connectionStatus) && !!entry.remote.connectionStatus.vscodeUpgradeMethod;
const actionLayout = `${showConfig}:${showDocumentation}:${showConnection}:${showDelete}:${showLocationPreference}:${showUpdate}`;
if (template.column === 'actions' && template.actionLayout !== actionLayout) {
template.actionLayout = actionLayout;
template.toolbar.setActions(showConnection ? [template.action, template.rename, ...(showDelete ? [template.delete] : [])] : [
template.toolbar.setActions(showConnection ? [
Comment on lines 301 to +303
template.action, template.rename, template.copyAddress,
...(showLocationPreference ? [template.locationPreference] : []),
...(showDelete ? [template.delete] : []),
...(showUpdate ? [template.updateServer] : []),
] : [
...(showDocumentation ? [template.documentation] : []),
...(showConfig ? [template.action] : []),
]);
Expand Down Expand Up @@ -321,8 +343,15 @@ export class RemotesTableRenderer implements ITableRenderer<IRemoteTableRow, Rem
? localize('remotes.connectUnavailable', "Connect to {0}: {1}", entry.remote.name, entry.remote.unavailableReason) : template.action.label;
template.rename.label = localize('remotes.rename', "Rename {0}...", entry.remote.name);
template.rename.enabled = !entry.busy;
template.copyAddress.label = localize('remotes.copyAddress', "Copy Address of {0}", entry.remote.name);
template.locationPreference.label = localize('remotes.locationPreference', "Change Preferred Agent Location for {0}", entry.remote.name);
template.locationPreference.enabled = !entry.busy
&& !RemoteAgentHostConnectionStatus.isConnecting(entry.remote.connectionStatus)
&& !RemoteAgentHostConnectionStatus.isReconnecting(entry.remote.connectionStatus);
template.delete.label = localize('remotes.delete', "Delete {0}", entry.remote.name);
template.delete.enabled = showDelete && !entry.busy;
template.updateServer.label = localize('remotes.updateServer', "Update Server for {0}", entry.remote.name);
template.updateServer.enabled = showUpdate && !entry.busy;
}
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class RemotesWidget extends Disposable {
{ id: 'gutter', label: '', weight: 0, minimumWidth: 64, maximumWidth: 64 },
{ id: 'name', label: localize('remotes.nameColumn', "Name"), weight: 0.8, minimumWidth: 220 },
{ id: 'connection', label: localize('remotes.connectionColumn', "Connection"), weight: 0.2, minimumWidth: 144 },
{ id: 'actions', label: '', weight: 0, minimumWidth: 72, maximumWidth: 72 },
{ id: 'actions', label: '', weight: 0, minimumWidth: 144, maximumWidth: 144 },
] as const;
const renderers = columnDefinitions.map(column => new RemotesTableRenderer(
column.id, (entry, action) => this.runRowAction(entry, action), actionHoverDelegate,
Expand Down Expand Up @@ -268,9 +268,15 @@ export class RemotesWidget extends Disposable {
} else if (action === 'connect') {
void this.setConnected(entry.remote.id, !RemoteAgentHostConnectionStatus.isConnected(entry.remote.connectionStatus));
} else if (action === 'rename') {
void this.renameRemote(entry.remote.id);
} else if (action === 'delete' && entry.remote.kind === 'environment' && entry.remote.canRemove) {
void this.deleteEnvironment(entry.remote.id);
void this.manageRemote(entry.remote.id, 'rename');
} else if (action === 'copyAddress') {
void this.copyAddress(entry.remote.id);
} else if (action === 'locationPreference' && (entry.remote.kind === 'tunnel' || entry.remote.kind === 'ssh')) {
void this.manageRemote(entry.remote.id, 'locationPreference');
} else if (action === 'updateServer') {
void this.manageRemote(entry.remote.id, 'updateServer');
} else if (action === 'delete' && (entry.remote.kind === 'environment' && entry.remote.canRemove || entry.remote.kind === 'tunnel' && entry.remote.canDelete)) {
void this.deleteRemote(entry.remote.id, entry.remote.kind);
}
}
}
Expand Down Expand Up @@ -474,14 +480,32 @@ export class RemotesWidget extends Disposable {
this.layout();
}

private async renameRemote(id: string): Promise<void> {
private async copyAddress(id: string): Promise<void> {
try {
await this.model.copyAddress(id);
} catch (error) {
if (!isCancellationError(error)) {
this.notificationService.error(error);
}
}
}

private async manageRemote(id: string, action: 'rename' | 'locationPreference' | 'updateServer'): Promise<void> {
if (this.pendingActions.has(id) || this.pendingConnections.has(id)) {
return;
}
const focusedAction = dom.getActiveElement();
const restoreFocus = dom.isHTMLElement(focusedAction) && this.domNode.contains(focusedAction) && focusedAction.closest<HTMLElement>('[data-remote-id]')?.dataset.remoteId === id;
this.pendingActions.add(id);
this.render();
try {
await this.model.rename(id);
if (action === 'locationPreference') {
await this.model.changePreferredAgentLocation(id);
} else if (action === 'updateServer') {
await this.model.updateServer(id);
} else {
await this.model.rename(id);
}
} catch (error) {
if (!isCancellationError(error)) {
this.notificationService.error(error);
Expand All @@ -493,15 +517,16 @@ export class RemotesWidget extends Disposable {
const active = dom.getActiveElement();
const activeRow = dom.isHTMLElement(active) && this.domNode.contains(active) ? active.closest<HTMLElement>('[data-remote-id]') : undefined;
const index = this.rows.findIndex(row => row.id === id);
if (activeRow && activeRow.dataset.remoteId !== id && index >= 0) {
if (index >= 0 && (activeRow && activeRow.dataset.remoteId !== id
|| restoreFocus && !focusedAction?.isConnected && active === this.domNode.ownerDocument.body)) {
this.table.setFocus([index]);
this.table.domFocus();
}
}
}
}

private async deleteEnvironment(id: string): Promise<void> {
private async deleteRemote(id: string, kind: 'environment' | 'tunnel'): Promise<void> {
if (this.pendingActions.has(id) || this.pendingConnections.has(id)) {
return;
}
Expand All @@ -510,7 +535,11 @@ export class RemotesWidget extends Disposable {
this.pendingActions.add(id);
this.render();
try {
await this.model.remove(id);
if (kind === 'tunnel') {
await this.model.deleteTunnel(id);
} else {
await this.model.remove(id);
}
} catch (error) {
if (!isCancellationError(error)) {
this.notificationService.error(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class RemotesAccessibleView implements IAccessibleViewImplementation {
localize('remotes.help.navigation', "Type in Search to filter remotes by name, address, or connection type. Clear Search and Collapse All are at the right of the search field; use Tab to reach these actions and Left and Right Arrow to move between them. Clear Search returns focus to the input. Collapse All preserves the search text. Groups without matches are hidden while searching. Use Down Arrow from Search to enter the table. Use Up and Down Arrow to navigate rows. On a group, use Left and Right Arrow to collapse or expand it, or press Enter to toggle it. Visibility icons are always shown; other row actions appear on hover or keyboard focus. On a remote, press Tab to reach its actions and use Left and Right Arrow to move between Connect or Disconnect and Rename. Use Enter or Space to activate an action. Resize columns by dragging their boundaries. Narrow windows scroll horizontally."),
localize('remotes.help.connection', "Connection labels include Connected, Connecting, and Disconnected. A warning indicates that a connection you requested failed. Hover the warning or open the Accessible View to read the error. Automatic connection failures do not create a warning, but automatic retries retain an existing warning until they finish. A failed retry updates the error; a successful retry clears it. Environments whose owning application is not online show Offline. Use Connect or Disconnect to manage the connection. Connection actions are unavailable while connecting or reconnecting. Disconnect follows the existing remote behavior and may remove it from the picker."),
localize('remotes.help.visibility', "Use the eye action at the start of an eligible remote row to show or hide it in the Environment and workspace pickers. Focus the row and press Tab, then Enter or Space to toggle it. Hidden remotes remain in this editor with a dimmed name and a Show action. Remotes that cannot currently appear have a disabled unavailable indicator instead of an eye; hover it or open the Accessible View for the reason. Newly discovered remotes must first be connected, and offline shared environments must come online. Your hidden preference is retained while a remote is unavailable. Choosing a folder through a remote subpicker shows that remote again. Hiding a remote does not prevent sending an existing draft when the remote is available and connected. This preference is local to your profile on this machine and does not connect, disconnect, or remove sessions."),
localize('remotes.help.manage', "Rename changes the remote's display name in this client. Leave the name empty to restore its default. Environment rows also offer Delete after Rename. Use the arrow keys to reach it in the row actions. Other remote types do not offer Delete; manage their SSH configuration, WSL distribution, or tunnel at the source."),
localize('remotes.help.manage', "Rename changes the remote's display name in this client. Leave the name empty to restore its default. Copy Address copies a remote's connection address without connecting, including while it is offline or busy. SSH and tunnel rows offer Change Preferred Agent Location, which uses the existing choice of a dedicated process or remote editor window and reconnects after saving. Cancelling leaves the preference unchanged. Incompatible hosts offer Update Server when they advertise an update method. This reports progress and reconnects after the server restarts. Environment and supported tunnel rows also offer Delete with confirmation. Deleting a tunnel removes its service-side registration and disconnects its clients, without deleting remote files. A tunnel currently hosted by this application must be stopped before it can be deleted. A deleted tunnel may reappear if a machine starts hosting it again. Use the arrow keys to reach these row actions. Other remote types do not offer Delete; manage their SSH configuration or WSL distribution at the source."),
localize('remotes.help.discovery', "Refresh looks for remotes without connecting. Its icon spins while discovery is running. Each group also shows a spinner after its count while that group's discovery is running. A failed group immediately shows a warning instead of its spinner, even if other groups are still loading. The tooltip and group label explain the problem; the Window log contains diagnostic details. If tunnel sign-in permissions are missing, a Sign In action appears instead of an error. Focus that row and press Tab, then Enter or Space to sign in and grant access to your tunnels. Sign In is disabled with a spinner while sign-in is in progress. This refreshes the list without connecting to a remote. Known locations remain available, and Refresh retries discovery."),
localize('remotes.help.setup', "Empty groups show setup guidance and a documentation link. Focus the placeholder row and press Tab to reach its link. Groups with documentation also have an info action that opens the same guide, even when they contain remotes or are collapsed. Focus the group and use Tab and the arrow keys to reach its actions. The SSH settings action opens your user SSH config in a modal editor and creates it if necessary. The Tunnels group has an Open Tunnel Settings action. After configuring a host, return to Remotes and choose Refresh."),
].join('\n\n') : editor.getAccessibilityContent(),
Expand Down
Loading
Loading