@@ -22,7 +22,7 @@ namespace Coder.Desktop.App.ViewModels;
22
22
23
23
public interface IAgentViewModelFactory
24
24
{
25
- public AgentViewModel Create ( Uuid id , string hostname , string hostnameSuffix ,
25
+ public AgentViewModel Create ( IAgentExpanderHost expanderHost , Uuid id , string hostname , string hostnameSuffix ,
26
26
AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string ? workspaceName ) ;
27
27
}
28
28
@@ -32,12 +32,12 @@ public class AgentViewModelFactory(
32
32
ICredentialManager credentialManager ,
33
33
IAgentAppViewModelFactory agentAppViewModelFactory ) : IAgentViewModelFactory
34
34
{
35
- public AgentViewModel Create ( Uuid id , string hostname , string hostnameSuffix ,
35
+ public AgentViewModel Create ( IAgentExpanderHost expanderHost , Uuid id , string hostname , string hostnameSuffix ,
36
36
AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string ? workspaceName )
37
37
{
38
- return new AgentViewModel ( childLogger , coderApiClientFactory , credentialManager , agentAppViewModelFactory )
38
+ return new AgentViewModel ( childLogger , coderApiClientFactory , credentialManager , agentAppViewModelFactory ,
39
+ expanderHost , id )
39
40
{
40
- Id = id ,
41
41
Hostname = hostname ,
42
42
HostnameSuffix = hostnameSuffix ,
43
43
ConnectionStatus = connectionStatus ,
@@ -74,12 +74,14 @@ public partial class AgentViewModel : ObservableObject, IModelUpdateable<AgentVi
74
74
private readonly DispatcherQueue _dispatcherQueue =
75
75
DispatcherQueue . GetForCurrentThread ( ) ;
76
76
77
+ private readonly IAgentExpanderHost _expanderHost ;
78
+
77
79
// This isn't an ObservableProperty because the property itself never
78
80
// changes. We add an event listener for the collection changing in the
79
81
// constructor.
80
82
public readonly ObservableCollection < AgentAppViewModel > Apps = [ ] ;
81
83
82
- public required Uuid Id { get ; init ; }
84
+ public readonly Uuid Id ;
83
85
84
86
[ ObservableProperty ]
85
87
[ NotifyPropertyChangedFor ( nameof ( FullHostname ) ) ]
@@ -160,12 +162,28 @@ public string DashboardUrl
160
162
}
161
163
162
164
public AgentViewModel ( ILogger < AgentViewModel > logger , ICoderApiClientFactory coderApiClientFactory ,
163
- ICredentialManager credentialManager , IAgentAppViewModelFactory agentAppViewModelFactory )
165
+ ICredentialManager credentialManager , IAgentAppViewModelFactory agentAppViewModelFactory ,
166
+ IAgentExpanderHost expanderHost , Uuid id )
164
167
{
165
168
_logger = logger ;
166
169
_coderApiClientFactory = coderApiClientFactory ;
167
170
_credentialManager = credentialManager ;
168
171
_agentAppViewModelFactory = agentAppViewModelFactory ;
172
+ _expanderHost = expanderHost ;
173
+
174
+ Id = id ;
175
+
176
+ PropertyChanged += ( _ , args ) =>
177
+ {
178
+ if ( args . PropertyName == nameof ( IsExpanded ) )
179
+ {
180
+ _expanderHost . HandleAgentExpanded ( Id , IsExpanded ) ;
181
+
182
+ // Every time the drawer is expanded, re-fetch all apps.
183
+ if ( IsExpanded && ! FetchingApps )
184
+ FetchApps ( ) ;
185
+ }
186
+ } ;
169
187
170
188
// Since the property value itself never changes, we add event
171
189
// listeners for the underlying collection changing instead.
@@ -202,18 +220,15 @@ public bool TryApplyChanges(AgentViewModel model)
202
220
[ RelayCommand ]
203
221
private void ToggleExpanded ( )
204
222
{
205
- // TODO: this should bubble to every other agent in the list so only
206
- // one can be active at a time.
207
223
SetExpanded ( ! IsExpanded ) ;
208
224
}
209
225
210
226
public void SetExpanded ( bool expanded )
211
227
{
228
+ if ( IsExpanded == expanded ) return ;
229
+ // This will bubble up to the TrayWindowViewModel because of the
230
+ // PropertyChanged handler.
212
231
IsExpanded = expanded ;
213
-
214
- // Every time the drawer is expanded, re-fetch all apps.
215
- if ( expanded && ! FetchingApps )
216
- FetchApps ( ) ;
217
232
}
218
233
219
234
partial void OnConnectionStatusChanged ( AgentConnectionStatus oldValue , AgentConnectionStatus newValue )
@@ -226,7 +241,26 @@ private void FetchApps()
226
241
if ( FetchingApps ) return ;
227
242
FetchingApps = true ;
228
243
229
- var client = _coderApiClientFactory . Create ( _credentialManager ) ;
244
+ // If the workspace is off, then there's no agent and there's no apps.
245
+ if ( ConnectionStatus == AgentConnectionStatus . Gray )
246
+ {
247
+ FetchingApps = false ;
248
+ Apps . Clear ( ) ;
249
+ return ;
250
+ }
251
+
252
+ // API client creation could fail, which would leave FetchingApps true.
253
+ ICoderApiClient client ;
254
+ try
255
+ {
256
+ client = _coderApiClientFactory . Create ( _credentialManager ) ;
257
+ }
258
+ catch
259
+ {
260
+ FetchingApps = false ;
261
+ throw ;
262
+ }
263
+
230
264
var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 15 ) ) ;
231
265
client . GetWorkspaceAgent ( Id . ToString ( ) , cts . Token ) . ContinueWith ( t =>
232
266
{
@@ -265,18 +299,24 @@ private void ContinueFetchApps(Task<WorkspaceAgent> task)
265
299
continue ;
266
300
}
267
301
268
- if ( string . IsNullOrEmpty ( app . Url ) )
302
+ if ( ! Uri . TryCreate ( app . Url , UriKind . Absolute , out var appUri ) )
269
303
{
270
- _logger . LogWarning ( "App URI '{Url}' for '{DisplayName}' is empty, app will not appear in list" , app . Url ,
304
+ _logger . LogWarning ( "Could not parse app URI '{Url}' for '{DisplayName}', app will not appear in list" ,
305
+ app . Url ,
271
306
app . DisplayName ) ;
272
307
continue ;
273
308
}
274
309
310
+ // HTTP or HTTPS external apps are usually things like
311
+ // wikis/documentation, which clutters up the app.
312
+ if ( appUri . Scheme is "http" or "https" )
313
+ continue ;
314
+
275
315
// Icon parse failures are not fatal, we will just use the fallback
276
316
// icon.
277
317
_ = Uri . TryCreate ( DashboardBaseUrl , app . Icon , out var iconUrl ) ;
278
318
279
- apps . Add ( _agentAppViewModelFactory . Create ( uuid , app . DisplayName , app . Url , iconUrl ) ) ;
319
+ apps . Add ( _agentAppViewModelFactory . Create ( uuid , app . DisplayName , appUri , iconUrl ) ) ;
280
320
}
281
321
282
322
foreach ( var displayApp in workspaceAgent . DisplayApps )
@@ -296,7 +336,22 @@ private void ContinueFetchApps(Task<WorkspaceAgent> task)
296
336
scheme = "vscode-insiders" ;
297
337
}
298
338
299
- var appUri = $ "{ scheme } ://vscode-remote/ssh-remote+{ FullHostname } /{ workspaceAgent . ExpandedDirectory } ";
339
+ Uri appUri ;
340
+ try
341
+ {
342
+ appUri = new UriBuilder
343
+ {
344
+ Scheme = scheme ,
345
+ Host = "vscode-remote" ,
346
+ Path = $ "/ssh-remote+{ FullHostname } /{ workspaceAgent . ExpandedDirectory } ",
347
+ } . Uri ;
348
+ }
349
+ catch ( Exception e )
350
+ {
351
+ _logger . LogWarning ( "Could not craft app URI for display app {displayApp}, app will not appear in list" ,
352
+ displayApp ) ;
353
+ continue ;
354
+ }
300
355
301
356
// Icon parse failures are not fatal, we will just use the fallback
302
357
// icon.
0 commit comments