@@ -51,6 +51,9 @@ export const REMOTE_TUNNEL_CONNECTION_STATE = new RawContextKey<CONTEXT_KEY_STAT
51
51
52
52
const SESSION_ID_STORAGE_KEY = 'remoteTunnelAccountPreference' ;
53
53
54
+ const REMOTE_TUNNEL_USED_STORAGE_KEY = 'remoteTunnelServiceUsed' ;
55
+ const REMOTE_TUNNEL_EXTENSION_RECOMMENDED_KEY = 'remoteTunnelExtensionRecommended' ;
56
+
54
57
type ExistingSessionItem = { session : AuthenticationSession ; providerId : string ; label : string ; description : string } ;
55
58
type IAuthenticationProvider = { id : string ; scopes : string [ ] } ;
56
59
type AuthenticationProviderOption = IQuickPickItem & { provider : IAuthenticationProvider } ;
@@ -103,6 +106,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
103
106
@ICommandService private commandService : ICommandService ,
104
107
@IWorkspaceContextService private workspaceContextService : IWorkspaceContextService ,
105
108
@IProgressService private progressService : IProgressService ,
109
+ @INotificationService private notificationService : INotificationService
106
110
) {
107
111
super ( ) ;
108
112
@@ -148,6 +152,8 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
148
152
registerLogChannel ( LOG_CHANNEL_ID , localize ( 'remoteTunnelLog' , "Remote Tunnel Service" ) , remoteTunnelServiceLogResource , fileService , logService ) ;
149
153
150
154
this . initialize ( ) ;
155
+
156
+ this . recommendRemoteExtensionIfNeeded ( ) ;
151
157
}
152
158
153
159
private get existingSessionId ( ) {
@@ -163,6 +169,68 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
163
169
}
164
170
}
165
171
172
+ private async recommendRemoteExtensionIfNeeded ( ) {
173
+ const remoteExtension = this . serverConfiguration . extension ;
174
+ const shouldRecommend = async ( ) => {
175
+ if ( this . storageService . getBoolean ( REMOTE_TUNNEL_EXTENSION_RECOMMENDED_KEY , StorageScope . APPLICATION ) ) {
176
+ return false ;
177
+ }
178
+ if ( await this . extensionService . getExtension ( remoteExtension . extensionId ) ) {
179
+ return false ;
180
+ }
181
+ const usedOnHost = this . storageService . get ( REMOTE_TUNNEL_USED_STORAGE_KEY , StorageScope . APPLICATION ) ;
182
+ if ( ! usedOnHost ) {
183
+ return false ;
184
+ }
185
+ const currentHostName = await this . remoteTunnelService . getHostName ( ) ;
186
+ if ( ! currentHostName || currentHostName === usedOnHost ) {
187
+ return false ;
188
+ }
189
+ return usedOnHost ;
190
+ } ;
191
+ const recommed = async ( ) => {
192
+ const usedOnHost = await shouldRecommend ( ) ;
193
+ if ( ! usedOnHost ) {
194
+ return false ;
195
+ }
196
+ this . notificationService . notify ( {
197
+ severity : Severity . Info ,
198
+ message :
199
+ localize (
200
+ {
201
+ key : 'recommend.remoteExtension' ,
202
+ comment : [ '{0} will be a host name, {1} will the link address to the web UI, {6} an extension name. [label](command:commandId) is a markdown link. Only translate the label, do not modify the format' ]
203
+ } ,
204
+ "'{0}' has turned on remote access. The {1} extension can be used to connect to it." ,
205
+ usedOnHost , remoteExtension . friendlyName
206
+ ) ,
207
+ actions : {
208
+ primary : [
209
+ new Action ( 'showExtension' , localize ( 'action.showExtension' , "Show Extension" ) , undefined , true , ( ) => {
210
+ return this . commandService . executeCommand ( 'workbench.extensions.action.showExtensionsWithIds' , [ remoteExtension . extensionId ] ) ;
211
+ } ) ,
212
+ new Action ( 'doNotShowAgain' , localize ( 'action.doNotShowAgain' , "Do not show again" ) , undefined , true , ( ) => {
213
+ this . storageService . store ( REMOTE_TUNNEL_EXTENSION_RECOMMENDED_KEY , true , StorageScope . APPLICATION , StorageTarget . USER ) ;
214
+ } ) ,
215
+ ]
216
+ }
217
+ } ) ;
218
+ return true ;
219
+ } ;
220
+ if ( await shouldRecommend ( ) ) {
221
+ const storageListener = this . storageService . onDidChangeValue ( async e => {
222
+ if ( e . key === REMOTE_TUNNEL_USED_STORAGE_KEY ) {
223
+ const success = await recommed ( ) ;
224
+ if ( success ) {
225
+ storageListener . dispose ( ) ;
226
+ }
227
+
228
+ }
229
+ } ) ;
230
+ }
231
+ }
232
+
233
+
166
234
private async initialize ( ) : Promise < void > {
167
235
const status = await this . remoteTunnelService . getTunnelStatus ( ) ;
168
236
if ( status . type === 'connected' ) {
@@ -418,6 +486,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
418
486
const notificationService = accessor . get ( INotificationService ) ;
419
487
const clipboardService = accessor . get ( IClipboardService ) ;
420
488
const commandService = accessor . get ( ICommandService ) ;
489
+ const storageService = accessor . get ( IStorageService ) ;
421
490
422
491
const connectionInfo = await that . startTunnel ( false ) ;
423
492
if ( connectionInfo ) {
@@ -443,6 +512,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo
443
512
]
444
513
}
445
514
} ) ;
515
+ storageService . store ( REMOTE_TUNNEL_USED_STORAGE_KEY , connectionInfo . hostName , StorageScope . APPLICATION , StorageTarget . USER ) ;
446
516
} else {
447
517
await notificationService . notify ( {
448
518
severity : Severity . Info ,
@@ -667,7 +737,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
667
737
[ CONFIGURATION_KEY_HOST_NAME ] : {
668
738
description : localize ( 'remoteTunnelAccess.machineName' , "The name under which the remote tunnel access is registered. If not set, the host name is used." ) ,
669
739
type : 'string' ,
670
- scope : ConfigurationScope . APPLICATION ,
740
+ scope : ConfigurationScope . MACHINE ,
671
741
pattern : '^[\\w-]*$' ,
672
742
patternErrorMessage : localize ( 'remoteTunnelAccess.machineNameRegex' , "The name can only consist of letters, numbers, underscore and minus." ) ,
673
743
maxLength : 20 ,
0 commit comments