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,8 +41,11 @@ 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 {
48+ System . Diagnostics . Debug . WriteLine ( $ "Creating agent: { didP2p } { preferredDerp } { latency } { lastHandshake } ") ;
4549 return new AgentViewModel ( childLogger , coderApiClientFactory , credentialManager , agentAppViewModelFactory ,
4650 expanderHost , id )
4751 {
@@ -51,6 +55,11 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu
5155 ConnectionStatus = connectionStatus ,
5256 DashboardBaseUrl = dashboardBaseUrl ,
5357 WorkspaceName = workspaceName ,
58+ DidP2p = didP2p ,
59+ PreferredDerp = preferredDerp ,
60+ Latency = latency ,
61+ PreferredDerpLatency = preferredDerpLatency ,
62+ LastHandshake = lastHandshake ,
5463 } ;
5564 }
5665
@@ -73,10 +82,10 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id,
7382
7483public enum AgentConnectionStatus
7584{
76- Green ,
77- Yellow ,
78- Red ,
79- Gray ,
85+ Healthy ,
86+ Unhealthy ,
87+ NoRecentHandshake ,
88+ Offline ,
8089}
8190
8291public partial class AgentViewModel : ObservableObject , IModelUpdateable < AgentViewModel >
@@ -182,6 +191,75 @@ public string FullyQualifiedDomainName
182191 [ NotifyPropertyChangedFor ( nameof ( ExpandAppsMessage ) ) ]
183192 public partial bool AppFetchErrored { get ; set ; } = false ;
184193
194+ [ ObservableProperty ]
195+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
196+ public partial bool ? DidP2p { get ; set ; } = false ;
197+
198+ [ ObservableProperty ]
199+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
200+ public partial string ? PreferredDerp { get ; set ; } = null ;
201+
202+ [ ObservableProperty ]
203+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
204+ public partial TimeSpan ? Latency { get ; set ; } = null ;
205+
206+ [ ObservableProperty ]
207+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
208+ public partial TimeSpan ? PreferredDerpLatency { get ; set ; } = null ;
209+
210+ [ ObservableProperty ]
211+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
212+ public partial DateTime ? LastHandshake { get ; set ; } = null ;
213+
214+ public string ConnectionTooltip { get
215+ {
216+ var description = new StringBuilder ( ) ;
217+
218+ if ( DidP2p != null && DidP2p . Value && Latency != null )
219+ {
220+ description . Append ( $ """
221+ You're connected peer-to-peer.
222+
223+ You ↔ { Latency . Value . Milliseconds } ms ↔ { WorkspaceName }
224+ """
225+ ) ;
226+ }
227+ else if ( PreferredDerpLatency != null )
228+ {
229+ description . Append ( $ """
230+ You're connected through a DERP relay.
231+ We'll switch over to peer-to-peer when available.
232+
233+ Total latency: { PreferredDerpLatency . Value . Milliseconds } ms
234+ """
235+ ) ;
236+
237+ if ( PreferredDerp != null && Latency != null )
238+ {
239+ description . Append ( $ "\n You ↔ { PreferredDerp } : { PreferredDerpLatency . Value . Milliseconds } ms") ;
240+
241+ var derpToWorkspaceEstimatedLatency = Latency - PreferredDerpLatency ;
242+
243+ // Guard against negative values if the two readings were taken at different times
244+ if ( derpToWorkspaceEstimatedLatency > TimeSpan . Zero )
245+ {
246+ description . Append ( $ "\n { PreferredDerp } ms ↔ { WorkspaceName } : { derpToWorkspaceEstimatedLatency . Value . Milliseconds } ms") ;
247+ }
248+ }
249+ }
250+ if ( LastHandshake != null )
251+ description . Append ( $ "\n \n Last handshake: { LastHandshake ? . ToString ( ) ?? "Unknown" } ") ;
252+
253+ var tooltip = description . ToString ( ) . TrimEnd ( '\n ' , ' ' ) ;
254+
255+ if ( string . IsNullOrEmpty ( tooltip ) )
256+ return "No connection information available." ;
257+
258+ return tooltip ;
259+ }
260+ }
261+
262+
185263 // We only show 6 apps max, which fills the entire width of the tray
186264 // window.
187265 public IEnumerable < AgentAppViewModel > VisibleApps => Apps . Count > MaxAppsPerRow ? Apps . Take ( MaxAppsPerRow ) : Apps ;
@@ -192,7 +270,7 @@ public string? ExpandAppsMessage
192270 {
193271 get
194272 {
195- if ( ConnectionStatus == AgentConnectionStatus . Gray )
273+ if ( ConnectionStatus == AgentConnectionStatus . Offline )
196274 return "Your workspace is offline." ;
197275 if ( FetchingApps && Apps . Count == 0 )
198276 // Don't show this message if we have any apps already. When
@@ -285,6 +363,16 @@ public bool TryApplyChanges(AgentViewModel model)
285363 DashboardBaseUrl = model . DashboardBaseUrl ;
286364 if ( WorkspaceName != model . WorkspaceName )
287365 WorkspaceName = model . WorkspaceName ;
366+ if ( DidP2p != model . DidP2p )
367+ DidP2p = model . DidP2p ;
368+ if ( PreferredDerp != model . PreferredDerp )
369+ PreferredDerp = model . PreferredDerp ;
370+ if ( Latency != model . Latency )
371+ Latency = model . Latency ;
372+ if ( PreferredDerpLatency != model . PreferredDerpLatency )
373+ PreferredDerpLatency = model . PreferredDerpLatency ;
374+ if ( LastHandshake != model . LastHandshake )
375+ LastHandshake = model . LastHandshake ;
288376
289377 // Apps are not set externally.
290378
@@ -307,7 +395,7 @@ public void SetExpanded(bool expanded)
307395
308396 partial void OnConnectionStatusChanged ( AgentConnectionStatus oldValue , AgentConnectionStatus newValue )
309397 {
310- if ( IsExpanded && newValue is not AgentConnectionStatus . Gray ) FetchApps ( ) ;
398+ if ( IsExpanded && newValue is not AgentConnectionStatus . Offline ) FetchApps ( ) ;
311399 }
312400
313401 private void FetchApps ( )
@@ -316,7 +404,7 @@ private void FetchApps()
316404 FetchingApps = true ;
317405
318406 // If the workspace is off, then there's no agent and there's no apps.
319- if ( ConnectionStatus == AgentConnectionStatus . Gray )
407+ if ( ConnectionStatus == AgentConnectionStatus . Offline )
320408 {
321409 FetchingApps = false ;
322410 Apps . Clear ( ) ;
0 commit comments