1- using System ;
2- using System . Collections . Generic ;
3- using System . Collections . ObjectModel ;
4- using System . ComponentModel ;
5- using System . Linq ;
6- using System . Threading ;
7- using System . Threading . Tasks ;
8- using Windows . ApplicationModel . DataTransfer ;
91using Coder . Desktop . App . Services ;
102using Coder . Desktop . App . Utils ;
113using Coder . Desktop . CoderSdk ;
1810using Microsoft . UI . Xaml ;
1911using Microsoft . UI . Xaml . Controls ;
2012using Microsoft . UI . Xaml . Controls . Primitives ;
13+ using System ;
14+ using System . Collections . Generic ;
15+ using System . Collections . ObjectModel ;
16+ using System . ComponentModel ;
17+ using System . Linq ;
18+ using System . Text ;
19+ using System . Threading ;
20+ using System . Threading . Tasks ;
21+ using System . Xml . Linq ;
22+ using Windows . ApplicationModel . DataTransfer ;
2123
2224namespace Coder . Desktop . App . ViewModels ;
2325
2426public interface IAgentViewModelFactory
2527{
2628 public AgentViewModel Create ( IAgentExpanderHost expanderHost , Uuid id , string fullyQualifiedDomainName ,
27- string hostnameSuffix ,
28- AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string ? workspaceName ) ;
29-
29+ string hostnameSuffix , AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl ,
30+ string ? workspaceName , bool ? didP2p , string ? preferredDerp , TimeSpan ? latency , TimeSpan ? preferredDerpLatency , DateTime ? lastHandshake ) ;
3031 public AgentViewModel CreateDummy ( IAgentExpanderHost expanderHost , Uuid id ,
3132 string hostnameSuffix ,
3233 AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string workspaceName ) ;
@@ -40,7 +41,9 @@ public class AgentViewModelFactory(
4041{
4142 public AgentViewModel Create ( IAgentExpanderHost expanderHost , Uuid id , string fullyQualifiedDomainName ,
4243 string hostnameSuffix ,
43- AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string ? workspaceName )
44+ AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl ,
45+ string ? workspaceName , bool ? didP2p , string ? preferredDerp , TimeSpan ? latency , TimeSpan ? preferredDerpLatency ,
46+ DateTime ? lastHandshake )
4447 {
4548 return new AgentViewModel ( childLogger , coderApiClientFactory , credentialManager , agentAppViewModelFactory ,
4649 expanderHost , id )
@@ -51,6 +54,11 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu
5154 ConnectionStatus = connectionStatus ,
5255 DashboardBaseUrl = dashboardBaseUrl ,
5356 WorkspaceName = workspaceName ,
57+ DidP2p = didP2p ,
58+ PreferredDerp = preferredDerp ,
59+ Latency = latency ,
60+ PreferredDerpLatency = preferredDerpLatency ,
61+ LastHandshake = lastHandshake ,
5462 } ;
5563 }
5664
@@ -73,10 +81,25 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id,
7381
7482public enum AgentConnectionStatus
7583{
76- Green ,
77- Yellow ,
78- Red ,
79- Gray ,
84+ Healthy ,
85+ Connecting ,
86+ Unhealthy ,
87+ NoRecentHandshake ,
88+ Offline
89+ }
90+
91+ public static class AgentConnectionStatusExtensions
92+ {
93+ public static string ToDisplayString ( this AgentConnectionStatus status ) =>
94+ status switch
95+ {
96+ AgentConnectionStatus . Healthy => "Healthy" ,
97+ AgentConnectionStatus . Connecting => "Connecting" ,
98+ AgentConnectionStatus . Unhealthy => "High latency" ,
99+ AgentConnectionStatus . NoRecentHandshake => "No recent handshake" ,
100+ AgentConnectionStatus . Offline => "Offline" ,
101+ _ => status . ToString ( )
102+ } ;
80103}
81104
82105public partial class AgentViewModel : ObservableObject , IModelUpdateable < AgentViewModel >
@@ -160,6 +183,7 @@ public string FullyQualifiedDomainName
160183 [ ObservableProperty ]
161184 [ NotifyPropertyChangedFor ( nameof ( ShowExpandAppsMessage ) ) ]
162185 [ NotifyPropertyChangedFor ( nameof ( ExpandAppsMessage ) ) ]
186+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
163187 public required partial AgentConnectionStatus ConnectionStatus { get ; set ; }
164188
165189 [ ObservableProperty ]
@@ -182,6 +206,77 @@ public string FullyQualifiedDomainName
182206 [ NotifyPropertyChangedFor ( nameof ( ExpandAppsMessage ) ) ]
183207 public partial bool AppFetchErrored { get ; set ; } = false ;
184208
209+ [ ObservableProperty ]
210+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
211+ public partial bool ? DidP2p { get ; set ; } = false ;
212+
213+ [ ObservableProperty ]
214+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
215+ public partial string ? PreferredDerp { get ; set ; } = null ;
216+
217+ [ ObservableProperty ]
218+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
219+ public partial TimeSpan ? Latency { get ; set ; } = null ;
220+
221+ [ ObservableProperty ]
222+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
223+ public partial TimeSpan ? PreferredDerpLatency { get ; set ; } = null ;
224+
225+ [ ObservableProperty ]
226+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
227+ public partial DateTime ? LastHandshake { get ; set ; } = null ;
228+
229+ public string ConnectionTooltip
230+ {
231+ get
232+ {
233+ var description = new StringBuilder ( ) ;
234+ var highLatencyWarning = ConnectionStatus == AgentConnectionStatus . Unhealthy ? $ "({ AgentConnectionStatus . Unhealthy . ToDisplayString ( ) } )" : "" ;
235+
236+ if ( DidP2p != null && DidP2p . Value && Latency != null )
237+ {
238+ description . Append ( $ """
239+ You're connected peer-to-peer. { highLatencyWarning }
240+
241+ You ↔ { Latency . Value . Milliseconds } ms ↔ { WorkspaceName }
242+ """
243+ ) ;
244+ }
245+ else if ( Latency != null )
246+ {
247+ description . Append ( $ """
248+ You're connected through a DERP relay. { highLatencyWarning }
249+ We'll switch over to peer-to-peer when available.
250+
251+ Total latency: { Latency . Value . Milliseconds } ms
252+ """
253+ ) ;
254+
255+ if ( PreferredDerpLatency != null )
256+ {
257+ description . Append ( $ "\n You ↔ { PreferredDerp } : { PreferredDerpLatency . Value . Milliseconds } ms") ;
258+
259+ var derpToWorkspaceEstimatedLatency = Latency - PreferredDerpLatency ;
260+
261+ // Guard against negative values if the two readings were taken at different times
262+ if ( derpToWorkspaceEstimatedLatency > TimeSpan . Zero )
263+ {
264+ description . Append ( $ "\n { PreferredDerp } ms ↔ { WorkspaceName } : { derpToWorkspaceEstimatedLatency . Value . Milliseconds } ms") ;
265+ }
266+ }
267+ }
268+ else
269+ {
270+ description . Append ( ConnectionStatus . ToDisplayString ( ) ) ;
271+ }
272+ if ( LastHandshake != null )
273+ description . Append ( $ "\n \n Last handshake: { LastHandshake ? . ToString ( ) } ") ;
274+
275+ return description . ToString ( ) . TrimEnd ( '\n ' , ' ' ) ; ;
276+ }
277+ }
278+
279+
185280 // We only show 6 apps max, which fills the entire width of the tray
186281 // window.
187282 public IEnumerable < AgentAppViewModel > VisibleApps => Apps . Count > MaxAppsPerRow ? Apps . Take ( MaxAppsPerRow ) : Apps ;
@@ -192,7 +287,7 @@ public string? ExpandAppsMessage
192287 {
193288 get
194289 {
195- if ( ConnectionStatus == AgentConnectionStatus . Gray )
290+ if ( ConnectionStatus == AgentConnectionStatus . Offline )
196291 return "Your workspace is offline." ;
197292 if ( FetchingApps && Apps . Count == 0 )
198293 // Don't show this message if we have any apps already. When
@@ -285,6 +380,16 @@ public bool TryApplyChanges(AgentViewModel model)
285380 DashboardBaseUrl = model . DashboardBaseUrl ;
286381 if ( WorkspaceName != model . WorkspaceName )
287382 WorkspaceName = model . WorkspaceName ;
383+ if ( DidP2p != model . DidP2p )
384+ DidP2p = model . DidP2p ;
385+ if ( PreferredDerp != model . PreferredDerp )
386+ PreferredDerp = model . PreferredDerp ;
387+ if ( Latency != model . Latency )
388+ Latency = model . Latency ;
389+ if ( PreferredDerpLatency != model . PreferredDerpLatency )
390+ PreferredDerpLatency = model . PreferredDerpLatency ;
391+ if ( LastHandshake != model . LastHandshake )
392+ LastHandshake = model . LastHandshake ;
288393
289394 // Apps are not set externally.
290395
@@ -307,7 +412,7 @@ public void SetExpanded(bool expanded)
307412
308413 partial void OnConnectionStatusChanged ( AgentConnectionStatus oldValue , AgentConnectionStatus newValue )
309414 {
310- if ( IsExpanded && newValue is not AgentConnectionStatus . Gray ) FetchApps ( ) ;
415+ if ( IsExpanded && newValue is not AgentConnectionStatus . Offline ) FetchApps ( ) ;
311416 }
312417
313418 private void FetchApps ( )
@@ -316,7 +421,7 @@ private void FetchApps()
316421 FetchingApps = true ;
317422
318423 // If the workspace is off, then there's no agent and there's no apps.
319- if ( ConnectionStatus == AgentConnectionStatus . Gray )
424+ if ( ConnectionStatus == AgentConnectionStatus . Offline )
320425 {
321426 FetchingApps = false ;
322427 Apps . Clear ( ) ;
0 commit comments