diff --git a/Coder-Desktop/Coder-Desktop/Theme.swift b/Coder-Desktop/Coder-Desktop/Theme.swift
index 1c15b086..546242c2 100644
--- a/Coder-Desktop/Coder-Desktop/Theme.swift
+++ b/Coder-Desktop/Coder-Desktop/Theme.swift
@@ -13,5 +13,9 @@ enum Theme {
         static let appIconSize: CGSize = .init(width: appIconWidth, height: appIconHeight)
     }
 
+    enum Animation {
+        static let collapsibleDuration = 0.2
+    }
+
     static let defaultVisibleAgents = 5
 }
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/Agents.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/Agents.swift
index 0ca65759..fb3928f6 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPN/Agents.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/Agents.swift
@@ -4,6 +4,8 @@ struct Agents<VPN: VPNService>: View {
     @EnvironmentObject var vpn: VPN
     @EnvironmentObject var state: AppState
     @State private var viewAll = false
+    @State private var expandedItem: VPNMenuItem.ID?
+    @State private var hasToggledExpansion: Bool = false
     private let defaultVisibleRows = 5
 
     let inspection = Inspection<Self>()
@@ -15,8 +17,24 @@ struct Agents<VPN: VPNService>: View {
                 let items = vpn.menuState.sorted
                 let visibleItems = viewAll ? items[...] : items.prefix(defaultVisibleRows)
                 ForEach(visibleItems, id: \.id) { agent in
-                    MenuItemView(item: agent, baseAccessURL: state.baseAccessURL!)
-                        .padding(.horizontal, Theme.Size.trayMargin)
+                    MenuItemView(
+                        item: agent,
+                        baseAccessURL: state.baseAccessURL!,
+                        expandedItem: $expandedItem,
+                        userInteracted: $hasToggledExpansion
+                    )
+                    .padding(.horizontal, Theme.Size.trayMargin)
+                }.onChange(of: visibleItems) {
+                    // If no workspaces are online, we should expand the first one to come online
+                    if visibleItems.filter({ $0.status != .off }).isEmpty {
+                        hasToggledExpansion = false
+                        return
+                    }
+                    if hasToggledExpansion {
+                        return
+                    }
+                    expandedItem = visibleItems.first?.id
+                    hasToggledExpansion = true
                 }
                 if items.count == 0 {
                     Text("No workspaces!")
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
index 1bc0b98b..d67e34ff 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
@@ -35,6 +35,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
         }
     }
 
+    func primaryHost(hostnameSuffix: String) -> String {
+        switch self {
+        case let .agent(agent): agent.primaryHost
+        case .offlineWorkspace: "\(wsName).\(hostnameSuffix)"
+        }
+    }
+
     static func < (lhs: VPNMenuItem, rhs: VPNMenuItem) -> Bool {
         switch (lhs, rhs) {
         case let (.agent(lhsAgent), .agent(rhsAgent)):
@@ -52,23 +59,23 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
 
 struct MenuItemView: View {
     @EnvironmentObject var state: AppState
+    @Environment(\.openURL) private var openURL
 
     private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VPNMenu")
 
     let item: VPNMenuItem
     let baseAccessURL: URL
+    @Binding var expandedItem: VPNMenuItem.ID?
+    @Binding var userInteracted: Bool
 
     @State private var nameIsSelected: Bool = false
-    @State private var copyIsSelected: Bool = false
 
-    private let defaultVisibleApps = 5
     @State private var apps: [WorkspaceApp] = []
 
+    var hasApps: Bool { !apps.isEmpty }
+
     private var itemName: AttributedString {
-        let name = switch item {
-        case let .agent(agent): agent.primaryHost
-        case .offlineWorkspace: "\(item.wsName).\(state.hostnameSuffix)"
-        }
+        let name = item.primaryHost(hostnameSuffix: state.hostnameSuffix)
 
         var formattedName = AttributedString(name)
         formattedName.foregroundColor = .primary
@@ -79,17 +86,34 @@ struct MenuItemView: View {
         return formattedName
     }
 
+    private var isExpanded: Bool {
+        expandedItem == item.id
+    }
+
     private var wsURL: URL {
         // TODO: CoderVPN currently only supports owned workspaces
         baseAccessURL.appending(path: "@me").appending(path: item.wsName)
     }
 
+    private func toggleExpanded() {
+        userInteracted = true
+        if isExpanded {
+            withAnimation(.snappy(duration: Theme.Animation.collapsibleDuration)) {
+                expandedItem = nil
+            }
+        } else {
+            withAnimation(.snappy(duration: Theme.Animation.collapsibleDuration)) {
+                expandedItem = item.id
+            }
+        }
+    }
+
     var body: some View {
         VStack(spacing: 0) {
-            HStack(spacing: 0) {
-                Link(destination: wsURL) {
+            HStack(spacing: 3) {
+                Button(action: toggleExpanded) {
                     HStack(spacing: Theme.Size.trayPadding) {
-                        StatusDot(color: item.status.color)
+                        AnimatedChevron(isExpanded: isExpanded, color: .secondary)
                         Text(itemName).lineLimit(1).truncationMode(.tail)
                         Spacer()
                     }.padding(.horizontal, Theme.Size.trayPadding)
@@ -98,42 +122,24 @@ struct MenuItemView: View {
                         .foregroundStyle(nameIsSelected ? .white : .primary)
                         .background(nameIsSelected ? Color.accentColor.opacity(0.8) : .clear)
                         .clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius))
-                        .onHoverWithPointingHand { hovering in
+                        .onHover { hovering in
                             nameIsSelected = hovering
                         }
-                    Spacer()
-                }.buttonStyle(.plain)
-                if case let .agent(agent) = item {
-                    Button {
-                        NSPasteboard.general.clearContents()
-                        NSPasteboard.general.setString(agent.primaryHost, forType: .string)
-                    } label: {
-                        Image(systemName: "doc.on.doc")
-                            .symbolVariant(.fill)
-                            .padding(3)
-                            .contentShape(Rectangle())
-                    }.foregroundStyle(copyIsSelected ? .white : .primary)
-                        .imageScale(.small)
-                        .background(copyIsSelected ? Color.accentColor.opacity(0.8) : .clear)
-                        .clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius))
-                        .onHoverWithPointingHand { hovering in copyIsSelected = hovering }
-                        .buttonStyle(.plain)
-                        .padding(.trailing, Theme.Size.trayMargin)
-                }
+                }.buttonStyle(.plain).padding(.trailing, 3)
+                MenuItemIcons(item: item, wsURL: wsURL)
             }
-            if !apps.isEmpty {
-                HStack(spacing: 17) {
-                    ForEach(apps.prefix(defaultVisibleApps), id: \.id) { app in
-                        WorkspaceAppIcon(app: app)
-                            .frame(width: Theme.Size.appIconWidth, height: Theme.Size.appIconHeight)
-                    }
-                    if apps.count < defaultVisibleApps {
-                        Spacer()
+            if isExpanded {
+                if hasApps {
+                    MenuItemCollapsibleView(apps: apps)
+                } else {
+                    HStack {
+                        Text(item.status == .off ? "Workspace is offline." : "No apps available.")
+                            .font(.body)
+                            .foregroundColor(.secondary)
+                            .padding(.horizontal, Theme.Size.trayInset)
+                            .padding(.top, 7)
                     }
                 }
-                .padding(.leading, apps.count < defaultVisibleApps ? 14 : 0)
-                .padding(.bottom, 5)
-                .padding(.top, 10)
             }
         }
         .task { await loadApps() }
@@ -172,3 +178,83 @@ struct MenuItemView: View {
         }
     }
 }
+
+struct MenuItemCollapsibleView: View {
+    private let defaultVisibleApps = 5
+    let apps: [WorkspaceApp]
+
+    var body: some View {
+        HStack(spacing: 17) {
+            ForEach(apps.prefix(defaultVisibleApps), id: \.id) { app in
+                WorkspaceAppIcon(app: app)
+                    .frame(width: Theme.Size.appIconWidth, height: Theme.Size.appIconHeight)
+            }
+            if apps.count < defaultVisibleApps {
+                Spacer()
+            }
+        }
+        .padding(.leading, apps.count < defaultVisibleApps ? 14 : 0)
+        .padding(.bottom, 5)
+        .padding(.top, 10)
+    }
+}
+
+struct MenuItemIcons: View {
+    @EnvironmentObject var state: AppState
+    @Environment(\.openURL) private var openURL
+
+    let item: VPNMenuItem
+    let wsURL: URL
+
+    @State private var copyIsSelected: Bool = false
+    @State private var webIsSelected: Bool = false
+
+    func copyToClipboard() {
+        let primaryHost = item.primaryHost(hostnameSuffix: state.hostnameSuffix)
+        NSPasteboard.general.clearContents()
+        NSPasteboard.general.setString(primaryHost, forType: .string)
+    }
+
+    var body: some View {
+        StatusDot(color: item.status.color)
+            .padding(.trailing, 3)
+            .padding(.top, 1)
+        MenuItemIconButton(systemName: "doc.on.doc", action: copyToClipboard)
+            .font(.system(size: 9))
+            .symbolVariant(.fill)
+        MenuItemIconButton(systemName: "globe", action: { openURL(wsURL) })
+            .contentShape(Rectangle())
+            .font(.system(size: 12))
+            .padding(.trailing, Theme.Size.trayMargin)
+    }
+}
+
+struct MenuItemIconButton: View {
+    let systemName: String
+    @State var isSelected: Bool = false
+    let action: @MainActor () -> Void
+
+    var body: some View {
+        Button(action: action) {
+            Image(systemName: systemName)
+                .padding(3)
+                .contentShape(Rectangle())
+        }.foregroundStyle(isSelected ? .white : .primary)
+            .background(isSelected ? Color.accentColor.opacity(0.8) : .clear)
+            .clipShape(.rect(cornerRadius: Theme.Size.rectCornerRadius))
+            .onHover { hovering in isSelected = hovering }
+            .buttonStyle(.plain)
+    }
+}
+
+struct AnimatedChevron: View {
+    let isExpanded: Bool
+    let color: Color
+
+    var body: some View {
+        Image(systemName: "chevron.right")
+            .font(.system(size: 12, weight: .semibold))
+            .foregroundColor(color)
+            .rotationEffect(.degrees(isExpanded ? 90 : 0))
+    }
+}
diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/WorkspaceAppIcon.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/WorkspaceAppIcon.swift
index 70a20d8b..14a4bd0f 100644
--- a/Coder-Desktop/Coder-Desktop/Views/VPN/WorkspaceAppIcon.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/VPN/WorkspaceAppIcon.swift
@@ -37,7 +37,7 @@ struct WorkspaceAppIcon: View {
             RoundedRectangle(cornerRadius: Theme.Size.rectCornerRadius * 2)
                 .stroke(.secondary, lineWidth: 1)
                 .opacity(isHovering && !isPressed ? 0.6 : 0.3)
-        ).onHoverWithPointingHand { hovering in isHovering = hovering }
+        ).onHover { hovering in isHovering = hovering }
         .simultaneousGesture(
             DragGesture(minimumDistance: 0)
                 .onChanged { _ in
diff --git a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
index 62c1607f..741b32e5 100644
--- a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift
@@ -62,7 +62,7 @@ struct AgentsTests {
         let forEach = try view.inspect().find(ViewType.ForEach.self)
         #expect(forEach.count == Theme.defaultVisibleAgents)
         // Agents are sorted by status, and then by name in alphabetical order
-        #expect(throws: Never.self) { try view.inspect().find(link: "a1.coder") }
+        #expect(throws: Never.self) { try view.inspect().find(text: "a1.coder") }
     }
 
     @Test
@@ -115,7 +115,7 @@ struct AgentsTests {
             try await sut.inspection.inspect { view in
                 let forEach = try view.find(ViewType.ForEach.self)
                 #expect(forEach.count == Theme.defaultVisibleAgents)
-                #expect(throws: Never.self) { try view.find(link: "offline.coder") }
+                #expect(throws: Never.self) { try view.find(text: "offline.coder") }
             }
         }
     }