From 88ae5f93a9a05e63a9b99405e3b76f4b79363af0 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Fri, 6 Jun 2025 16:09:06 +1000
Subject: [PATCH 1/4] feat: include ping and network stats on status tooltip

---
 .../Coder-Desktop/Coder_DesktopApp.swift      |   3 +
 Coder-Desktop/Coder-Desktop/Theme.swift       |   1 +
 .../Coder-Desktop/VPN/MenuState.swift         | 167 +++++++++++++++++-
 .../Coder-Desktop/Views/VPN/VPNMenuItem.swift |   8 +
 .../Coder-DesktopTests/AgentsTests.swift      |   1 +
 .../VPNMenuStateTests.swift                   |  34 +++-
 Coder-Desktop/VPN/Manager.swift               |   3 +-
 .../VPNLib/FileSync/FileSyncManagement.swift  |   3 -
 Coder-Desktop/VPNLib/vpn.pb.swift             | 112 ++++++++++++
 Coder-Desktop/VPNLib/vpn.proto                |  16 ++
 10 files changed, 337 insertions(+), 11 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index 3080e8c1..de12c6e1 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -84,6 +84,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     }
 
     func applicationDidFinishLaunching(_: Notification) {
+        // We have important file sync and network info behind tooltips,
+        // so the default delay is too long.
+        UserDefaults.standard.setValue(Theme.Animation.tooltipDelay, forKey: "NSInitialToolTipDelay")
         // Init SVG loader
         SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
 
diff --git a/Coder-Desktop/Coder-Desktop/Theme.swift b/Coder-Desktop/Coder-Desktop/Theme.swift
index c697f1e3..f5a2213f 100644
--- a/Coder-Desktop/Coder-Desktop/Theme.swift
+++ b/Coder-Desktop/Coder-Desktop/Theme.swift
@@ -15,6 +15,7 @@ enum Theme {
 
     enum Animation {
         static let collapsibleDuration = 0.2
+        static let tooltipDelay: Int = 250 // milliseconds
     }
 
     static let defaultVisibleAgents = 5
diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
index c989c1d7..29613225 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
@@ -1,4 +1,5 @@
 import Foundation
+import SwiftProtobuf
 import SwiftUI
 import VPNLib
 
@@ -9,6 +10,29 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
     let hosts: [String]
     let wsName: String
     let wsID: UUID
+    let lastPing: LastPing?
+    let lastHandshake: Date?
+
+    init(id: UUID,
+         name: String,
+         status: AgentStatus,
+         hosts: [String],
+         wsName: String,
+         wsID: UUID,
+         lastPing: LastPing? = nil,
+         lastHandshake: Date? = nil,
+         primaryHost: String)
+    {
+        self.id = id
+        self.name = name
+        self.status = status
+        self.hosts = hosts
+        self.wsName = wsName
+        self.wsID = wsID
+        self.lastPing = lastPing
+        self.lastHandshake = lastHandshake
+        self.primaryHost = primaryHost
+    }
 
     // Agents are sorted by status, and then by name
     static func < (lhs: Agent, rhs: Agent) -> Bool {
@@ -18,14 +42,82 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
         return lhs.wsName.localizedCompare(rhs.wsName) == .orderedAscending
     }
 
+    var statusString: String {
+        if status == .error {
+            return status.description
+        }
+
+        guard let lastPing else {
+            // either:
+            // - old coder deployment
+            // - we haven't received any pings yet
+            return status.description
+        }
+
+        var str: String
+        if lastPing.didP2p {
+            str = """
+            You're connected peer-to-peer.
+
+            You ↔ \(lastPing.latency.prettyPrintMs) ↔ \(wsName)
+            """
+        } else {
+            str = """
+            You're connected through a DERP relay.
+            We'll switch over to peer-to-peer when available.
+
+            Total latency: \(lastPing.latency.prettyPrintMs)
+            """
+            // We're not guranteed to have the preferred DERP latency
+            if let preferredDerpLatency = lastPing.preferredDerpLatency {
+                str += "\nYou ↔ \(lastPing.preferredDerp): \(preferredDerpLatency.prettyPrintMs)"
+                let derpToWorkspaceEstLatency = lastPing.latency - preferredDerpLatency
+                // We're not guaranteed the preferred derp latency is less than
+                // the total, as they might have been recorded at slightly
+                // different times, and we don't want to show a negative value.
+                if derpToWorkspaceEstLatency > 0 {
+                    str += "\n\(lastPing.preferredDerp) ↔ \(wsName): \(derpToWorkspaceEstLatency.prettyPrintMs)"
+                }
+            }
+        }
+        str += "\n\nLast handshake: \(lastHandshake?.relativeTimeString ?? "Unknown")"
+        return str
+    }
+
     let primaryHost: String
 }
 
+extension TimeInterval {
+    var prettyPrintMs: String {
+        Measurement(value: self * 1000, unit: UnitDuration.milliseconds)
+            .formatted(.measurement(width: .abbreviated,
+                                    numberFormatStyle: .number.precision(.fractionLength(2))))
+    }
+}
+
+struct LastPing: Equatable, Hashable {
+    let latency: TimeInterval
+    let didP2p: Bool
+    let preferredDerp: String
+    let preferredDerpLatency: TimeInterval?
+}
+
 enum AgentStatus: Int, Equatable, Comparable {
     case okay = 0
-    case warn = 1
-    case error = 2
-    case off = 3
+    case connecting = 1
+    case warn = 2
+    case error = 3
+    case off = 4
+
+    public var description: String {
+        switch self {
+        case .okay: "Connected"
+        case .connecting: "Connecting..."
+        case .warn: "Connected, but with high latency" // Currently unused
+        case .error: "Could not establish a connection to the agent. Retrying..."
+        case .off: "Offline"
+        }
+    }
 
     public var color: Color {
         switch self {
@@ -33,6 +125,7 @@ enum AgentStatus: Int, Equatable, Comparable {
         case .warn: .yellow
         case .error: .red
         case .off: .secondary
+        case .connecting: .yellow
         }
     }
 
@@ -87,14 +180,27 @@ struct VPNMenuState {
         workspace.agents.insert(id)
         workspaces[wsID] = workspace
 
+        var lastPing: LastPing?
+        if agent.hasLastPing {
+            lastPing = LastPing(
+                latency: agent.lastPing.latency.timeInterval,
+                didP2p: agent.lastPing.didP2P,
+                preferredDerp: agent.lastPing.preferredDerp,
+                preferredDerpLatency:
+                agent.lastPing.hasPreferredDerpLatency
+                    ? agent.lastPing.preferredDerpLatency.timeInterval
+                    : nil
+            )
+        }
         agents[id] = Agent(
             id: id,
             name: agent.name,
-            // If last handshake was not within last five minutes, the agent is unhealthy
-            status: agent.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .warn,
+            status: agent.status,
             hosts: nonEmptyHosts,
             wsName: workspace.name,
             wsID: wsID,
+            lastPing: lastPing,
+            lastHandshake: agent.lastHandshake.maybeDate,
             // Hosts arrive sorted by length, the shortest looks best in the UI.
             primaryHost: nonEmptyHosts.first!
         )
@@ -154,3 +260,54 @@ struct VPNMenuState {
         workspaces.removeAll()
     }
 }
+
+extension Date {
+    var relativeTimeString: String {
+        let formatter = RelativeDateTimeFormatter()
+        formatter.unitsStyle = .full
+        if Date.now.timeIntervalSince(self) < 1.0 {
+            // Instead of showing "in 0 seconds"
+            return "Just now"
+        }
+        return formatter.localizedString(for: self, relativeTo: Date.now)
+    }
+}
+
+extension SwiftProtobuf.Google_Protobuf_Timestamp {
+    var maybeDate: Date? {
+        guard seconds > 0 else { return nil }
+        return date
+    }
+}
+
+extension Vpn_Agent {
+    var healthyLastHandshakeMin: Date {
+        Date.now.addingTimeInterval(-500) // 5 minutes ago
+    }
+
+    var healthyPingMax: TimeInterval { 0.15 } // 150ms
+
+    var status: AgentStatus {
+        guard let lastHandshake = lastHandshake.maybeDate else {
+            // Initially the handshake is missing
+            return .connecting
+        }
+
+        return if lastHandshake < healthyLastHandshakeMin {
+            // If last handshake was not within the last five minutes, the agent
+            // is potentially unhealthy.
+            .error
+        } else if hasLastPing, lastPing.latency.timeInterval < healthyPingMax {
+            // If latency is less than 150ms
+            .okay
+        } else if hasLastPing, lastPing.latency.timeInterval >= healthyPingMax {
+            // if latency is greater than 150ms
+            .warn
+        } else {
+            // No ping data, but we have a recent handshake.
+            // We show green for backwards compatibility with old Coder
+            // deployments.
+            .okay
+        }
+    }
+}
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
index 3b92dc9d..7f681be0 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
@@ -21,6 +21,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
         }
     }
 
+    var statusString: String {
+        switch self {
+        case let .agent(agent): agent.statusString
+        case .offlineWorkspace: status.description
+        }
+    }
+
     var id: UUID {
         switch self {
         case let .agent(agent): agent.id
@@ -224,6 +231,7 @@ struct MenuItemIcons: View {
         StatusDot(color: item.status.color)
             .padding(.trailing, 3)
             .padding(.top, 1)
+            .help(item.statusString)
         MenuItemIconButton(systemName: "doc.on.doc", action: copyToClipboard)
             .font(.system(size: 9))
             .symbolVariant(.fill)
diff --git a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
index 741b32e5..8f84ab3d 100644
--- a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
@@ -28,6 +28,7 @@ struct AgentsTests {
                 hosts: ["a\($0).coder"],
                 wsName: "ws\($0)",
                 wsID: UUID(),
+                lastPing: nil,
                 primaryHost: "a\($0).coder"
             )
             return (agent.id, agent)
diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
index d82aff8e..e45deafe 100644
--- a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
@@ -18,6 +18,9 @@ struct VPNMenuStateTests {
             $0.workspaceID = workspaceID.uuidData
             $0.name = "dev"
             $0.lastHandshake = .init(date: Date.now)
+            $0.lastPing = .with {
+                $0.latency = .init(floatLiteral: 0.05)
+            }
             $0.fqdn = ["foo.coder"]
         }
 
@@ -72,6 +75,29 @@ struct VPNMenuStateTests {
         #expect(state.workspaces[workspaceID] == nil)
     }
 
+    @Test
+    mutating func testUpsertAgent_poorConnection() async throws {
+        let agentID = UUID()
+        let workspaceID = UUID()
+        state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" })
+
+        let agent = Vpn_Agent.with {
+            $0.id = agentID.uuidData
+            $0.workspaceID = workspaceID.uuidData
+            $0.name = "agent1"
+            $0.lastHandshake = .init(date: Date.now)
+            $0.lastPing = .with {
+                $0.latency = .init(seconds: 1)
+            }
+            $0.fqdn = ["foo.coder"]
+        }
+
+        state.upsertAgent(agent)
+
+        let storedAgent = try #require(state.agents[agentID])
+        #expect(storedAgent.status == .warn)
+    }
+
     @Test
     mutating func testUpsertAgent_unhealthyAgent() async throws {
         let agentID = UUID()
@@ -89,7 +115,7 @@ struct VPNMenuStateTests {
         state.upsertAgent(agent)
 
         let storedAgent = try #require(state.agents[agentID])
-        #expect(storedAgent.status == .warn)
+        #expect(storedAgent.status == .error)
     }
 
     @Test
@@ -114,6 +140,9 @@ struct VPNMenuStateTests {
             $0.workspaceID = workspaceID.uuidData
             $0.name = "agent1" // Same name as old agent
             $0.lastHandshake = .init(date: Date.now)
+            $0.lastPing = .with {
+                $0.latency = .init(floatLiteral: 0.05)
+            }
             $0.fqdn = ["foo.coder"]
         }
 
@@ -146,6 +175,9 @@ struct VPNMenuStateTests {
             $0.workspaceID = workspaceID.uuidData
             $0.name = "agent1"
             $0.lastHandshake = .init(date: Date.now.addingTimeInterval(-200))
+            $0.lastPing = .with {
+                $0.latency = .init(floatLiteral: 0.05)
+            }
             $0.fqdn = ["foo.coder"]
         }
         state.upsertAgent(agent)
diff --git a/Coder-Desktop/VPN/Manager.swift b/Coder-Desktop/VPN/Manager.swift
index 649a1612..952e301e 100644
--- a/Coder-Desktop/VPN/Manager.swift
+++ b/Coder-Desktop/VPN/Manager.swift
@@ -40,7 +40,6 @@ actor Manager {
                 dest: dest,
                 urlSession: URLSession(configuration: sessionConfig)
             ) { progress in
-                // TODO: Debounce, somehow
                 pushProgress(stage: .downloading, downloadProgress: progress)
             }
         } catch {
@@ -322,7 +321,7 @@ func writeVpnLog(_ log: Vpn_Log) {
         category: log.loggerNames.joined(separator: ".")
     )
     let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
-    logger.log(level: level, "\(log.message, privacy: .public): \(fields, privacy: .public)")
+    logger.log(level: level, "\(log.message, privacy: .public)\(fields.isEmpty ? "" : ": \(fields)", privacy: .public)")
 }
 
 private func removeQuarantine(_ dest: URL) async throws(ManagerError) {
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
index 80fa76ff..3ae85b87 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
@@ -47,9 +47,6 @@ public extension MutagenDaemon {
             }
         }
         do {
-            // The first creation will need to transfer the agent binary
-            // TODO: Because this is pretty long, we should show progress updates
-            // using the prompter messages
             _ = try await client!.sync.create(req, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout * 4)))
         } catch {
             throw .grpcFailure(error)
diff --git a/Coder-Desktop/VPNLib/vpn.pb.swift b/Coder-Desktop/VPNLib/vpn.pb.swift
index 3e728045..3f630d0e 100644
--- a/Coder-Desktop/VPNLib/vpn.pb.swift
+++ b/Coder-Desktop/VPNLib/vpn.pb.swift
@@ -520,11 +520,63 @@ public struct Vpn_Agent: @unchecked Sendable {
   /// Clears the value of `lastHandshake`. Subsequent reads from it will return its default value.
   public mutating func clearLastHandshake() {self._lastHandshake = nil}
 
+  /// If unset, a successful ping has not yet been made.
+  public var lastPing: Vpn_LastPing {
+    get {return _lastPing ?? Vpn_LastPing()}
+    set {_lastPing = newValue}
+  }
+  /// Returns true if `lastPing` has been explicitly set.
+  public var hasLastPing: Bool {return self._lastPing != nil}
+  /// Clears the value of `lastPing`. Subsequent reads from it will return its default value.
+  public mutating func clearLastPing() {self._lastPing = nil}
+
   public var unknownFields = SwiftProtobuf.UnknownStorage()
 
   public init() {}
 
   fileprivate var _lastHandshake: SwiftProtobuf.Google_Protobuf_Timestamp? = nil
+  fileprivate var _lastPing: Vpn_LastPing? = nil
+}
+
+public struct Vpn_LastPing: Sendable {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// latency is the RTT of the ping to the agent.
+  public var latency: SwiftProtobuf.Google_Protobuf_Duration {
+    get {return _latency ?? SwiftProtobuf.Google_Protobuf_Duration()}
+    set {_latency = newValue}
+  }
+  /// Returns true if `latency` has been explicitly set.
+  public var hasLatency: Bool {return self._latency != nil}
+  /// Clears the value of `latency`. Subsequent reads from it will return its default value.
+  public mutating func clearLatency() {self._latency = nil}
+
+  /// did_p2p indicates whether the ping was sent P2P, or over DERP.
+  public var didP2P: Bool = false
+
+  /// preferred_derp is the human readable name of the preferred DERP region,
+  /// or the region used for the last ping, if it was sent over DERP.
+  public var preferredDerp: String = String()
+
+  /// preferred_derp_latency is the last known latency to the preferred DERP
+  /// region. Unset if the region does not appear in the DERP map.
+  public var preferredDerpLatency: SwiftProtobuf.Google_Protobuf_Duration {
+    get {return _preferredDerpLatency ?? SwiftProtobuf.Google_Protobuf_Duration()}
+    set {_preferredDerpLatency = newValue}
+  }
+  /// Returns true if `preferredDerpLatency` has been explicitly set.
+  public var hasPreferredDerpLatency: Bool {return self._preferredDerpLatency != nil}
+  /// Clears the value of `preferredDerpLatency`. Subsequent reads from it will return its default value.
+  public mutating func clearPreferredDerpLatency() {self._preferredDerpLatency = nil}
+
+  public var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  public init() {}
+
+  fileprivate var _latency: SwiftProtobuf.Google_Protobuf_Duration? = nil
+  fileprivate var _preferredDerpLatency: SwiftProtobuf.Google_Protobuf_Duration? = nil
 }
 
 /// NetworkSettingsRequest is based on
@@ -1579,6 +1631,7 @@ extension Vpn_Agent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
     4: .same(proto: "fqdn"),
     5: .standard(proto: "ip_addrs"),
     6: .standard(proto: "last_handshake"),
+    7: .standard(proto: "last_ping"),
   ]
 
   public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@@ -1593,6 +1646,7 @@ extension Vpn_Agent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
       case 4: try { try decoder.decodeRepeatedStringField(value: &self.fqdn) }()
       case 5: try { try decoder.decodeRepeatedStringField(value: &self.ipAddrs) }()
       case 6: try { try decoder.decodeSingularMessageField(value: &self._lastHandshake) }()
+      case 7: try { try decoder.decodeSingularMessageField(value: &self._lastPing) }()
       default: break
       }
     }
@@ -1621,6 +1675,9 @@ extension Vpn_Agent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
     try { if let v = self._lastHandshake {
       try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
     } }()
+    try { if let v = self._lastPing {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
+    } }()
     try unknownFields.traverse(visitor: &visitor)
   }
 
@@ -1631,6 +1688,61 @@ extension Vpn_Agent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
     if lhs.fqdn != rhs.fqdn {return false}
     if lhs.ipAddrs != rhs.ipAddrs {return false}
     if lhs._lastHandshake != rhs._lastHandshake {return false}
+    if lhs._lastPing != rhs._lastPing {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Vpn_LastPing: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  public static let protoMessageName: String = _protobuf_package + ".LastPing"
+  public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "latency"),
+    2: .standard(proto: "did_p2p"),
+    3: .standard(proto: "preferred_derp"),
+    4: .standard(proto: "preferred_derp_latency"),
+  ]
+
+  public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularMessageField(value: &self._latency) }()
+      case 2: try { try decoder.decodeSingularBoolField(value: &self.didP2P) }()
+      case 3: try { try decoder.decodeSingularStringField(value: &self.preferredDerp) }()
+      case 4: try { try decoder.decodeSingularMessageField(value: &self._preferredDerpLatency) }()
+      default: break
+      }
+    }
+  }
+
+  public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    // The use of inline closures is to circumvent an issue where the compiler
+    // allocates stack space for every if/case branch local when no optimizations
+    // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
+    // https://github.com/apple/swift-protobuf/issues/1182
+    try { if let v = self._latency {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
+    } }()
+    if self.didP2P != false {
+      try visitor.visitSingularBoolField(value: self.didP2P, fieldNumber: 2)
+    }
+    if !self.preferredDerp.isEmpty {
+      try visitor.visitSingularStringField(value: self.preferredDerp, fieldNumber: 3)
+    }
+    try { if let v = self._preferredDerpLatency {
+      try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
+    } }()
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  public static func ==(lhs: Vpn_LastPing, rhs: Vpn_LastPing) -> Bool {
+    if lhs._latency != rhs._latency {return false}
+    if lhs.didP2P != rhs.didP2P {return false}
+    if lhs.preferredDerp != rhs.preferredDerp {return false}
+    if lhs._preferredDerpLatency != rhs._preferredDerpLatency {return false}
     if lhs.unknownFields != rhs.unknownFields {return false}
     return true
   }
diff --git a/Coder-Desktop/VPNLib/vpn.proto b/Coder-Desktop/VPNLib/vpn.proto
index b3fe54c5..59ea1933 100644
--- a/Coder-Desktop/VPNLib/vpn.proto
+++ b/Coder-Desktop/VPNLib/vpn.proto
@@ -3,6 +3,7 @@ option go_package = "github.com/coder/coder/v2/vpn";
 option csharp_namespace = "Coder.Desktop.Vpn.Proto";
 
 import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
 
 package vpn;
 
@@ -130,6 +131,21 @@ message Agent {
     // last_handshake is the primary indicator of whether we are connected to a peer. Zero value or
     // anything longer than 5 minutes ago means there is a problem.
     google.protobuf.Timestamp last_handshake = 6;
+    // If unset, a successful ping has not yet been made.
+    optional LastPing last_ping = 7;
+}
+
+message LastPing {
+    // latency is the RTT of the ping to the agent.
+    google.protobuf.Duration latency = 1;
+    // did_p2p indicates whether the ping was sent P2P, or over DERP.
+    bool did_p2p = 2;
+    // preferred_derp is the human readable name of the preferred DERP region,
+    // or the region used for the last ping, if it was sent over DERP.
+    string preferred_derp = 3;
+    // preferred_derp_latency is the last known latency to the preferred DERP
+    // region. Unset if the region does not appear in the DERP map.
+    optional google.protobuf.Duration preferred_derp_latency = 4;
 }
 
 // NetworkSettingsRequest is based on

From a00c350548ca4b06898395ff5057c7e4de3d17c6 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Fri, 6 Jun 2025 16:16:30 +1000
Subject: [PATCH 2/4] typo

---
 Coder-Desktop/Coder-Desktop/VPN/MenuState.swift | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
index 29613225..29870a10 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
@@ -282,7 +282,7 @@ extension SwiftProtobuf.Google_Protobuf_Timestamp {
 
 extension Vpn_Agent {
     var healthyLastHandshakeMin: Date {
-        Date.now.addingTimeInterval(-500) // 5 minutes ago
+        Date.now.addingTimeInterval(-300) // 5 minutes ago
     }
 
     var healthyPingMax: TimeInterval { 0.15 } // 150ms

From 92eab7efd3029fb1fcabb0177977a0daf7e69276 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Fri, 6 Jun 2025 16:44:12 +1000
Subject: [PATCH 3/4] extra tests

---
 .../Coder-Desktop/VPN/MenuState.swift         |  5 ++--
 .../VPNMenuStateTests.swift                   | 29 +++++++++++++++++++
 2 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
index 29870a10..4027ea75 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
@@ -89,9 +89,8 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
 
 extension TimeInterval {
     var prettyPrintMs: String {
-        Measurement(value: self * 1000, unit: UnitDuration.milliseconds)
-            .formatted(.measurement(width: .abbreviated,
-                                    numberFormatStyle: .number.precision(.fractionLength(2))))
+        let milliseconds = self * 1000
+        return "\(milliseconds.formatted(.number.precision(.fractionLength(2)))) ms"
     }
 }
 
diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
index e45deafe..3ee04321 100644
--- a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
@@ -20,6 +20,7 @@ struct VPNMenuStateTests {
             $0.lastHandshake = .init(date: Date.now)
             $0.lastPing = .with {
                 $0.latency = .init(floatLiteral: 0.05)
+                $0.didP2P = true
             }
             $0.fqdn = ["foo.coder"]
         }
@@ -32,6 +33,9 @@ struct VPNMenuStateTests {
         #expect(storedAgent.wsName == "foo")
         #expect(storedAgent.primaryHost == "foo.coder")
         #expect(storedAgent.status == .okay)
+        #expect(storedAgent.statusString.contains("You're connected peer-to-peer."))
+        #expect(storedAgent.statusString.contains("You ↔ 50.00 ms ↔ foo"))
+        #expect(storedAgent.statusString.contains("Last handshake: Just now"))
     }
 
     @Test
@@ -98,6 +102,26 @@ struct VPNMenuStateTests {
         #expect(storedAgent.status == .warn)
     }
 
+    @Test
+    mutating func testUpsertAgent_connecting() async throws {
+        let agentID = UUID()
+        let workspaceID = UUID()
+        state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" })
+
+        let agent = Vpn_Agent.with {
+            $0.id = agentID.uuidData
+            $0.workspaceID = workspaceID.uuidData
+            $0.name = "agent1"
+            $0.lastHandshake = .init()
+            $0.fqdn = ["foo.coder"]
+        }
+
+        state.upsertAgent(agent)
+
+        let storedAgent = try #require(state.agents[agentID])
+        #expect(storedAgent.status == .connecting)
+    }
+
     @Test
     mutating func testUpsertAgent_unhealthyAgent() async throws {
         let agentID = UUID()
@@ -176,6 +200,7 @@ struct VPNMenuStateTests {
             $0.name = "agent1"
             $0.lastHandshake = .init(date: Date.now.addingTimeInterval(-200))
             $0.lastPing = .with {
+                $0.didP2P = false
                 $0.latency = .init(floatLiteral: 0.05)
             }
             $0.fqdn = ["foo.coder"]
@@ -187,6 +212,10 @@ struct VPNMenuStateTests {
         #expect(output[0].id == agentID)
         #expect(output[0].wsName == "foo")
         #expect(output[0].status == .okay)
+        let storedAgentFromSort = try #require(state.agents[agentID])
+        #expect(storedAgentFromSort.statusString.contains("You're connected through a DERP relay."))
+        #expect(storedAgentFromSort.statusString.contains("Total latency: 50.00 ms"))
+        #expect(storedAgentFromSort.statusString.contains("Last handshake: 3 minutes ago"))
     }
 
     @Test

From 99d386823e570166bf6224bedea565d26800c922 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Fri, 6 Jun 2025 18:30:00 +1000
Subject: [PATCH 4/4] review

---
 .../Preview Content/PreviewVPN.swift          |  8 +--
 .../Coder-Desktop/VPN/MenuState.swift         | 58 +++++++++----------
 .../VPNMenuStateTests.swift                   |  4 +-
 3 files changed, 35 insertions(+), 35 deletions(-)

diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift
index 4d4e9f90..91d5bf5e 100644
--- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift	
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift	
@@ -5,21 +5,21 @@ import SwiftUI
 final class PreviewVPN: Coder_Desktop.VPNService {
     @Published var state: Coder_Desktop.VPNServiceState = .connected
     @Published var menuState: VPNMenuState = .init(agents: [
-        UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2",
+        UUID(): Agent(id: UUID(), name: "dev", status: .no_recent_handshake, hosts: ["asdf.coder"], wsName: "dogfood2",
                       wsID: UUID(), primaryHost: "asdf.coder"),
         UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"],
                       wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"),
-        UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc",
+        UUID(): Agent(id: UUID(), name: "dev", status: .high_latency, hosts: ["asdf.coder"], wsName: "opensrc",
                       wsID: UUID(), primaryHost: "asdf.coder"),
         UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor",
                       wsID: UUID(), primaryHost: "asdf.coder"),
         UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "example",
                       wsID: UUID(), primaryHost: "asdf.coder"),
-        UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2",
+        UUID(): Agent(id: UUID(), name: "dev", status: .no_recent_handshake, hosts: ["asdf.coder"], wsName: "dogfood2",
                       wsID: UUID(), primaryHost: "asdf.coder"),
         UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"],
                       wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"),
-        UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc",
+        UUID(): Agent(id: UUID(), name: "dev", status: .high_latency, hosts: ["asdf.coder"], wsName: "opensrc",
                       wsID: UUID(), primaryHost: "asdf.coder"),
         UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor",
                       wsID: UUID(), primaryHost: "asdf.coder"),
diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
index 4027ea75..d13be3c6 100644
--- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
+++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift
@@ -43,27 +43,32 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
     }
 
     var statusString: String {
-        if status == .error {
+        switch status {
+        case .okay, .high_latency:
+            break
+        default:
             return status.description
         }
 
         guard let lastPing else {
-            // either:
-            // - old coder deployment
-            // - we haven't received any pings yet
+            // Either:
+            // - Old coder deployment
+            // - We haven't received any pings yet
             return status.description
         }
 
+        let highLatencyWarning = status == .high_latency ? "(High latency)" : ""
+
         var str: String
         if lastPing.didP2p {
             str = """
-            You're connected peer-to-peer.
+            You're connected peer-to-peer. \(highLatencyWarning)
 
             You ↔ \(lastPing.latency.prettyPrintMs) ↔ \(wsName)
             """
         } else {
             str = """
-            You're connected through a DERP relay.
+            You're connected through a DERP relay. \(highLatencyWarning)
             We'll switch over to peer-to-peer when available.
 
             Total latency: \(lastPing.latency.prettyPrintMs)
@@ -104,16 +109,16 @@ struct LastPing: Equatable, Hashable {
 enum AgentStatus: Int, Equatable, Comparable {
     case okay = 0
     case connecting = 1
-    case warn = 2
-    case error = 3
+    case high_latency = 2
+    case no_recent_handshake = 3
     case off = 4
 
     public var description: String {
         switch self {
         case .okay: "Connected"
         case .connecting: "Connecting..."
-        case .warn: "Connected, but with high latency" // Currently unused
-        case .error: "Could not establish a connection to the agent. Retrying..."
+        case .high_latency: "Connected, but with high latency" // Message currently unused
+        case .no_recent_handshake: "Could not establish a connection to the agent. Retrying..."
         case .off: "Offline"
         }
     }
@@ -121,8 +126,8 @@ enum AgentStatus: Int, Equatable, Comparable {
     public var color: Color {
         switch self {
         case .okay: .green
-        case .warn: .yellow
-        case .error: .red
+        case .high_latency: .yellow
+        case .no_recent_handshake: .red
         case .off: .secondary
         case .connecting: .yellow
         }
@@ -287,26 +292,21 @@ extension Vpn_Agent {
     var healthyPingMax: TimeInterval { 0.15 } // 150ms
 
     var status: AgentStatus {
+        // Initially the handshake is missing
         guard let lastHandshake = lastHandshake.maybeDate else {
-            // Initially the handshake is missing
             return .connecting
         }
-
-        return if lastHandshake < healthyLastHandshakeMin {
-            // If last handshake was not within the last five minutes, the agent
-            // is potentially unhealthy.
-            .error
-        } else if hasLastPing, lastPing.latency.timeInterval < healthyPingMax {
-            // If latency is less than 150ms
-            .okay
-        } else if hasLastPing, lastPing.latency.timeInterval >= healthyPingMax {
-            // if latency is greater than 150ms
-            .warn
-        } else {
-            // No ping data, but we have a recent handshake.
-            // We show green for backwards compatibility with old Coder
-            // deployments.
-            .okay
+        // If last handshake was not within the last five minutes, the agent
+        // is potentially unhealthy.
+        guard lastHandshake >= healthyLastHandshakeMin else {
+            return .no_recent_handshake
+        }
+        // No ping data, but we have a recent handshake.
+        // We show green for backwards compatibility with old Coder
+        // deployments.
+        guard hasLastPing else {
+            return .okay
         }
+        return lastPing.latency.timeInterval < healthyPingMax ? .okay : .high_latency
     }
 }
diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
index 3ee04321..dbd61a93 100644
--- a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift
@@ -99,7 +99,7 @@ struct VPNMenuStateTests {
         state.upsertAgent(agent)
 
         let storedAgent = try #require(state.agents[agentID])
-        #expect(storedAgent.status == .warn)
+        #expect(storedAgent.status == .high_latency)
     }
 
     @Test
@@ -139,7 +139,7 @@ struct VPNMenuStateTests {
         state.upsertAgent(agent)
 
         let storedAgent = try #require(state.agents[agentID])
-        #expect(storedAgent.status == .error)
+        #expect(storedAgent.status == .no_recent_handshake)
     }
 
     @Test